Commits

Ralph Meijer  committed b713f44

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

  • Participants
  • Parent commits 736d818

Comments (0)

Files changed (6)

File c2s_server_factory.patch

 # HG changeset patch
-# Parent d76497171af8f3acf1efd2c8433fbdc3c4a55f92
+# Parent 23dbe722e4482b8286153d29bf87902b464f919f
 Add factory for accepting client connections.
 
 The new XMPPC2SServerFactory is a server factory for accepting client
  * Add docstrings.
  * Add tests.
 
-diff -r d76497171af8 wokkel/client.py
---- a/wokkel/client.py	Wed Nov 30 09:31:07 2011 +0100
-+++ b/wokkel/client.py	Wed Nov 30 09:32:01 2011 +0100
-@@ -20,6 +20,7 @@
+diff --git a/wokkel/client.py b/wokkel/client.py
+--- a/wokkel/client.py
++++ b/wokkel/client.py
+@@ -21,6 +21,7 @@
  from twisted.words.xish import domish
  
  from wokkel import generic
 +from wokkel.compat import XmlStreamServerFactory
+ from wokkel.iwokkel import IUserSession
  from wokkel.subprotocols import StreamManager
  
- NS_CLIENT = 'jabber:client'
-@@ -347,3 +348,98 @@
- 
- 
- 
-+class RecipientUnavailable(Exception):
-+    """
-+    The addressed entity is not, or no longer, available.
-+    """
+@@ -401,3 +402,61 @@
+             self.portal = self.portals[self.xmlstream.thisEntity]
+         except KeyError:
+             raise error.StreamError('host-unknown')
 +
 +
 +
 +class XMPPC2SServerFactory(XmlStreamServerFactory):
 +
-+    def __init__(self, service):
-+        self.service = service
++    def __init__(self, portal):
++        self.portal = portal
 +
 +        def authenticatorFactory():
-+            return XMPPClientListenAuthenticator(service)
++            return XMPPClientListenAuthenticator(portal)
 +
 +        XmlStreamServerFactory.__init__(self, authenticatorFactory)
 +        self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
 +                          self.onAuthenticated)
 +
 +        self.serial = 0
-+        self.streams = {}
 +
 +
 +    def onConnectionMade(self, xs):
 +            xs.rawDataInFn = logDataIn
 +            xs.rawDataOutFn = logDataOut
 +
++        xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost,
++                                                   0, xs)
 +        xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
 +
 +
 +    def onAuthenticated(self, xs):
 +        log.msg("Client connection %d authenticated" % xs.serial)
 +
-+        xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost,
-+                                                   0, xs)
-+        xs.addObserver('/*', self.onElement, 0, xs)
-+
-+        # Record this stream as bound to the authenticated JID
-+        self.streams[xs.otherEntity] = xs
++        xs.addObserver('/*', xs.avatar.send)
 +
 +
 +    def onConnectionLost(self, xs, reason):
 +        log.msg("Client connection %d disconnected" % xs.serial)
 +
-+        entity = xs.otherEntity
-+        self.service.unbindResource(entity.user,
-+                                    entity.host,
-+                                    entity.resource,
-+                                    reason)
-+
-+        # If the lost connections had been bound, remove the reference
-+        if xs.otherEntity in self.streams:
-+            del self.streams[xs.otherEntity]
-+
 +
 +    def onError(self, reason):
 +        log.err(reason, "Stream Error")
-+
-+
-+    def onElement(self, xs, stanza):
-+        """
-+        Called when an element was received from one of the connected streams.
-+
-+        """
-+        if stanza.handled:
-+            return
-+        else:
-+            self.service.onElement(stanza, xs.otherEntity)
-+
-+
-+    def deliverStanza(self, element, recipient):
-+        if recipient in self.streams:
-+            self.streams[recipient].send(element)
-+        else:
-+            raise RecipientUnavailable(u"There is no connection for %s" %
-+                                       recipient.full())

File c2s_stanza_handlers.patch

 # HG changeset patch
-# Parent 8c6fa8ea95402e5c968f57e7fde2f8e249c11d12
+# Parent 41c2620133080ea88612732ff211561c660cfdec
 Add c2s protocol handlers for iq, message and presence stanzas.
 
 TODO:
 new file mode 100644
 --- /dev/null
 +++ b/doc/examples/client_service.tac
-@@ -0,0 +1,75 @@
+@@ -0,0 +1,82 @@
 +from twisted.application import service, strports
++from twisted.cred.portal import Portal
++from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
 +from twisted.internet import defer
 +
 +from wokkel import client, xmppim
 +sessionManager = client.SessionManager(domain, accounts)
 +sessionManager.setHandlerParent(component)
 +
++checker = InMemoryUsernamePasswordDatabaseDontUse(ralphm='secret',
++                                                  termie='secret')
++portal = Portal(sessionManager, (checker,))
++portals = {JID(domain): portal}
++
 +xmppim.AccountIQHandler(sessionManager).setHandlerParent(component)
 +xmppim.AccountMessageHandler(sessionManager).setHandlerParent(component)
 +xmppim.PresenceServerHandler(sessionManager, domain, roster).setHandlerParent(component)
 +StaticRoster(roster).setHandlerParent(component)
 +PingHandler().setHandlerParent(component)
 +
-+c2sFactory = client.XMPPC2SServerFactory(sessionManager)
++c2sFactory = client.XMPPC2SServerFactory(portals)
 +c2sFactory.logTraffic = True
 +c2sService = strports.service('5224', c2sFactory)
 +c2sService.setServiceParent(application)

File client_listen_authenticator.patch

 # HG changeset patch
-# Parent ad0f4165244b1c661023f03d8f7557fe5352337f
+# Parent a1648553ea06b7f0b38fec775db71ac03782e3d6
 Add authenticator for accepting XMPP client connections.
 
 The new authenticator XMPPClientListenAuthenticator is to be used together
-with an `XmlStream` created for an incoming XMPP stream. It handles the
-SASL PLAIN mechanism only.
+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
 called with the desired resource name. Upon stream disconnect, the
 logout callback is called.
 
-TODO:
-
- * Add tests.
- * Add docstrings.
-
 diff --git a/wokkel/client.py b/wokkel/client.py
 --- a/wokkel/client.py
 +++ b/wokkel/client.py
-@@ -10,14 +10,28 @@
+@@ -10,14 +10,27 @@
  that should probably eventually move there.
  """
  
 +import base64
 +
-+from zope.interface import Interface
-+
  from twisted.application import service
 -from twisted.internet import reactor
 +from twisted.cred import credentials, error as ecred
 +from twisted.words.xish import domish
  
  from wokkel import generic
++from wokkel.iwokkel import IUserSession
  from wokkel.subprotocols import StreamManager
  
 +NS_CLIENT = 'jabber:client'
  class CheckAuthInitializer(object):
      """
      Check what authentication methods are available.
-@@ -51,7 +65,7 @@
+@@ -51,7 +64,7 @@
      autentication.
      """
  
  
      def __init__(self, jid, password):
          xmlstream.ConnectAuthenticator.__init__(self, jid.host)
-@@ -186,3 +200,210 @@
+@@ -186,3 +199,246 @@
      c = XMPPClientConnector(reactor, domain, factory)
      c.connect()
      return factory.deferred
 +    """
 +
 +
-+class IAccount(Interface):
-+    pass
 +
++class AuthorizationIdentifierNotSupported(Exception):
++    """
++    Authorization Identifiers are not supported.
++    """
 +
 +
-+class SASLReceivingInitializer(object):
++
++class SASLReceivingInitializer(generic.BaseReceivingInitializer):
++    """
++    Stream initializer for SASL authentication, receiving side.
++    """
++
 +    required = True
 +
-+    def __init__(self, xs, portal):
-+        self.xmlstream = xs
++    def __init__(self, name, xs, portal):
++        generic.BaseReceivingInitializer.__init__(self, name, xs)
 +        self.portal = portal
-+        self.deferred = defer.Deferred()
 +        self.failureGrace = 3
 +
 +
 +
 +
 +    def initialize(self):
-+        self.xmlstream.addObserver(XPATH_AUTH, self.onAuth)
++        self.xmlstream.avatar = None
++        self.xmlstream.addObserver(XPATH_AUTH, self._onAuth)
 +        return self.deferred
 +
 +
-+    def onAuth(self, auth):
++    def _onAuth(self, auth):
++        """
++        Called when the start of the SASL negotiation is received.
++
++        @type auth: L{domish.Element}.
++        """
 +        auth.handled = True
 +
 +        def cb(_):
 +            response = domish.Element((sasl.NS_XMPP_SASL, 'success'))
 +            self.xmlstream.send(response)
 +            self.xmlstream.reset()
++            self.deferred.callback(xmlstream.Reset)
 +
 +        def eb(failure):
 +            if failure.check(ecred.UnauthorizedLogin):
 +                condition = 'not-authorized'
 +            elif failure.check(InvalidMechanism):
 +                condition = 'invalid-mechanism'
++            elif failure.check(AuthorizationIdentifierNotSupported):
++                condition = 'invalid-authz'
 +            else:
 +                log.err(failure)
 +                condition = 'temporary-auth-failure'
 +            # Close stream on too many failing authentication attempts
 +            self.failureGrace -= 1
 +            if self.failureGrace == 0:
-+                raise error.StreamError('policy-violation')
-+                #self.xmlstream.sendStreamError(exc)
++                self.deferred.errback(error.StreamError('policy-violation'))
++            else:
++                return
 +
-+        d = defer.maybeDeferred(self.doAuth, auth)
++        d = defer.maybeDeferred(self._doAuth, auth)
 +        d.addCallbacks(cb, eb)
-+        d.chainDeferred(self.deferred)
 +
 +
-+    def credentialsFromPlain(self, auth):
++    def _credentialsFromPlain(self, auth):
++        """
++        Create credentials from the initial response for PLAIN.
++        """
 +        initialResponse = base64.b64decode(unicode(auth))
 +        authzid, authcid, passwd = initialResponse.split('\x00')
 +
-+        # FIXME: bail if authzid is set
++        if authzid:
++            raise AuthorizationIdentifierNotSupported()
 +
-+        creds = credentials.UsernamePassword(username=authcid.encode('utf-8'),
++        creds = credentials.UsernamePassword(username=authcid,
 +                                             password=passwd)
 +        return creds
 +
 +
-+    def doAuth(self, auth):
-+
++    def _doAuth(self, auth):
++        """
++        Start authentication.
++        """
 +        if auth.getAttribute('mechanism') != 'PLAIN':
 +            raise InvalidMechanism()
 +
-+        creds = self.credentialsFromPlain(auth)
++        creds = self._credentialsFromPlain(auth)
 +
 +        def cb((iface, avatar, logout)):
 +            self.xmlstream.avatar = avatar
 +            self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT,
 +                                       lambda _: logout())
 +
-+        d = self.portal.login(creds, None, IAccount)
++        d = self.portal.login(creds, self.xmlstream, IUserSession)
 +        d.addCallback(cb)
 +        return d
 +
 +
 +
-+class BindReceivingInitializer(object):
++class BindReceivingInitializer(generic.BaseReceivingInitializer):
++    """
++    Stream initializer for resource binding, receiving side.
++    """
++
 +    required = True
 +
-+    def __init__(self, xs):
-+        self.xmlstream = xs
-+        self.deferred = defer.Deferred()
-+
-+
 +    def getFeatures(self):
 +        feature = domish.Element((client.NS_XMPP_BIND, 'bind'))
 +        return [feature]
 +            response = xmlstream.toResponse(iq, 'result')
 +            response.addElement((client.NS_XMPP_BIND, 'bind'))
 +            response.bind.addElement((client.NS_XMPP_BIND, 'jid'),
-+                                  content=boundJID.full())
++                                     content=boundJID.full())
 +
 +            return response
 +
-+        def eb(failure):
-+            if not isinstance(failure, error.StanzaError):
-+                log.msg(failure)
-+                exc = error.StanzaError('internal-server-error')
-+            else:
-+                exc = failure.value
-+
-+            return exc.toResponse(iq)
-+
 +        iq.handled = True
 +        resource = unicode(iq.bind) or None
 +        d = self.xmlstream.avatar.bindResource(resource)
 +        d.addCallback(cb)
-+        d.addErrback(eb)
 +        d.addCallback(self.xmlstream.send)
 +        d.chainDeferred(self.deferred)
 +
 +
 +
-+class SessionReceivingInitializer(object):
++class SessionReceivingInitializer(generic.BaseReceivingInitializer):
++    """
++    Stream initializer for session establishment, receiving side.
++
++    This is mostly a no-op and just returns a result stanza. If resource
++    binding hasn't yet completed, this will return a stanza error with the
++    condition C{'forbidden'}.
++
++    Note that RFC 6120 deprecated the session establishment protocol. This
++    is provided for backwards compatibility.
++    """
++
 +    required = False
 +
-+    def __init__(self, xs):
-+        self.xmlstream = xs
-+        self.deferred = defer.Deferred()
-+
-+
 +    def getFeatures(self):
 +        feature = domish.Element((client.NS_XMPP_SESSION, 'session'))
 +        return [feature]
 +        reply = domish.Element((None, 'iq'))
 +
 +        if self.xmlstream.otherEntity:
-+            reply = xmlstream.toResponse(iq)
++            reply = xmlstream.toResponse(iq, 'result')
 +        else:
 +            reply = error.StanzaError('forbidden').toResponse(iq)
 +        self.xmlstream.send(reply)
 +
 +
 +class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator):
++    """
++    XML Stream authenticator for XMPP clients, server side.
++
++    @ivar portals: Mapping of server JIDs to Cred Portals.
++    @type portals: C{dict} of L{twisted.words.protocols.jabber.jid.JID} to
++        L{twisted.cred.portal.Portal}.
++    """
++
 +    namespace = NS_CLIENT
 +
 +    def __init__(self, portals):
 +
 +
 +    def getInitializers(self):
++        """
++        Return initializers based on previously completed initializers.
++
++        This has three stages: 1. SASL, 2. Resource binding and session
++        establishment. 3. Completed. Note that session establishment
++        is optional.
++        """
 +        if not self.completedInitializers:
-+            return [SASLReceivingInitializer(self.xmlstream, self.portal)]
-+        elif isinstance(self.completedInitializers[-1],
-+                        SASLReceivingInitializer):
-+            return [BindReceivingInitializer(self.xmlstream),
-+                    SessionReceivingInitializer(self.xmlstream)]
++            return [SASLReceivingInitializer('sasl', self.xmlstream, self.portal)]
++        elif self.completedInitializers[-1] == 'sasl':
++            return [BindReceivingInitializer('bind', self.xmlstream),
++                    SessionReceivingInitializer('session', self.xmlstream)]
 +        else:
 +            return []
 +
 +
 +    def checkStream(self):
++        """
++        Check that the stream header has proper addressing.
++
++        The C{'to'} attribute must be present and there should have a matching
++        portal in L{portals}.
++        """
 +        generic.FeatureListenAuthenticator.checkStream(self)
 +
 +        if not self.xmlstream.thisEntity:
 +            self.portal = self.portals[self.xmlstream.thisEntity]
 +        except KeyError:
 +            raise error.StreamError('host-unknown')
+diff --git a/wokkel/generic.py b/wokkel/generic.py
+--- a/wokkel/generic.py
++++ b/wokkel/generic.py
+@@ -465,6 +465,7 @@
+ 
+     def __init__(self):
+         self.completedInitializers = []
++        self._initialized = False
+ 
+ 
+     def _onElementFallback(self, element):
+@@ -556,11 +557,12 @@
+ 
+         self.xmlstream.send(features)
+ 
+-        if not required:
++        if not required and not self._initialized:
+             # There are no required initializers anymore. This stream is
+             # now ready for the exchange of stanzas.
+             self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
+             self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
++            self._initialized = True
+ 
+         if ds:
+             d = defer.DeferredList(ds, fireOnOneCallback=True,
+diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
+--- a/wokkel/iwokkel.py
++++ b/wokkel/iwokkel.py
+@@ -985,6 +985,45 @@
+ 
+ 
+ 
++class IUserSession(Interface):
++    def loggedIn(realm, mind):
++        """
++        Called by the realm when login occurs.
++
++        @param realm: The realm though which login is occurring.
++        @param mind: The mind object.
++        """
++
++
++    def bindResource(resource):
++        """
++        Bind a resource to this session.
++
++        @type resource: C{unicode}.
++        """
++
++
++    def logout():
++        """
++        End this session.
++
++        This is called when the stream is disconnected.
++        """
++
++
++    def send(element):
++        """
++        Called when the client sends a stanza.
++        """
++
++
++    def receive(element):
++        """
++        Have the client receive a stanza.
++        """
++
++
++
+ class IReceivingInitializer(Interface):
+     """
+     Interface for XMPP stream initializers for receiving entities.
 diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
 --- a/wokkel/test/test_client.py
 +++ b/wokkel/test/test_client.py
-@@ -5,14 +5,21 @@
+@@ -5,16 +5,29 @@
  Tests for L{wokkel.client}.
  """
  
++from base64 import b64encode
++
 +from zope.interface import implements
 +
 +from twisted.cred.portal import IRealm, Portal
 +from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
  from twisted.internet import defer
++from twisted.test import proto_helpers
  from twisted.trial import unittest
- from twisted.words.protocols.jabber import xmlstream
+-from twisted.words.protocols.jabber import xmlstream
++from twisted.words.protocols.jabber import error, xmlstream
++from twisted.words.protocols.jabber.client import NS_XMPP_BIND
++from twisted.words.protocols.jabber.client import NS_XMPP_SESSION
  from twisted.words.protocols.jabber.client import XMPPAuthenticator
  from twisted.words.protocols.jabber.jid import JID
 +from twisted.words.protocols.jabber.sasl import NS_XMPP_SASL
  from twisted.words.protocols.jabber.xmlstream import XMPPHandler
 +from twisted.words.xish import xpath
  
- from wokkel import client
+-from wokkel import client
++from wokkel import client, iwokkel
++from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator
  
-@@ -155,3 +162,167 @@
+ class XMPPClientTest(unittest.TestCase):
+     """
+@@ -155,3 +168,505 @@
          self.assertEqual(factory.deferred, d2)
  
          return d1
 +
 +
-+class TestAccount(object):
-+    implements(client.IAccount)
++
++class TestSession(object):
++    implements(iwokkel.IUserSession)
++
++    def __init__(self, domain, user):
++        self.domain = domain
++        self.user = user
++
++
++    def bindResource(self, resource):
++        return defer.succeed(JID(tuple=(self.user, self.domain, resource)))
++
 +
 +
 +class TestRealm(object):
 +
 +    logoutCalled = False
 +
++    def __init__(self, domain):
++        self.domain = domain
++
++
 +    def requestAvatar(self, avatarId, mind, *interfaces):
-+        return (client.IAccount, TestAccount(), self.logout)
++        return (iwokkel.IUserSession,
++                TestSession(self.domain, avatarId.decode('utf-8')),
++                self.logout)
 +
 +
 +    def logout(self):
 +        self.logoutCalled = True
 +
 +
-+class TestableXmlStream(xmlstream.XmlStream):
 +
-+    def __init__(self, authenticator):
-+        xmlstream.XmlStream.__init__(self, authenticator)
-+        self.headerSent = False
-+        self.footerSent = False
-+        self.streamErrors = []
-+        self.output = []
++class TestableFeatureListenAuthenticator(FeatureListenAuthenticator):
++    namespace = 'jabber:client'
 +
++    initialized = None
 +
-+    def reset(self):
-+        xmlstream.XmlStream.reset(self)
-+        self.headerSent = False
++    def __init__(self, getInitializers):
++        """
++        Set up authenticator.
 +
++        @param getInitializers: Function to override the getInitializers
++            method. It will receive C{self} as the only argument.
++        """
++        FeatureListenAuthenticator.__init__(self)
 +
-+    def sendHeader(self):
-+        self.headerSent = True
++        import types
++        self.getInitializers = types.MethodType(getInitializers, self)
 +
++        xs = TestableXmlStream(self)
++        xs.makeConnection(proto_helpers.StringTransport())
 +
-+    def sendFooter(self):
-+        self.footerSent = True
 +
++    def streamStarted(self, rootElement):
++        """
++        Set up observers for authentication events.
++        """
++        def authenticated(_):
++            self.initialized = True
 +
-+    def sendStreamError(self, streamError):
-+        self.streamErrors.append(streamError)
++        self.xmlstream.addObserver(STREAM_AUTHD_EVENT, authenticated)
++        FeatureListenAuthenticator.streamStarted(self, rootElement)
 +
 +
-+    def send(self, obj):
-+        self.output.append(obj)
 +
-+
-+class XMPPClientListenAuthenticatorTest(unittest.TestCase):
++class SASLReceivingInitializerTest(unittest.TestCase):
 +    """
-+    Tests for L{client.XMPPClientListenAuthenticator}.
++    Tests for L{client.SASLReceivingInitializer}.
 +    """
 +
 +    def setUp(self):
-+        self.output = []
-+        realm = TestRealm()
++        realm = TestRealm(u'example.org')
 +        checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret')
-+        portal = Portal(realm, (checker,))
-+        portals = {JID('example.org'): portal}
-+        self.authenticator = client.XMPPClientListenAuthenticator(portals)
-+        self.xmlstream = TestableXmlStream(self.authenticator)
-+        self.xmlstream.makeConnection(self)
++        self.portal = portal = Portal(realm, (checker,))
 +
++        def getInitializers(self):
++            self.initializer = client.SASLReceivingInitializer('sasl',
++                                                               self.xmlstream,
++                                                               portal)
++            return [self.initializer]
 +
-+    def loseConnection(self):
++        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
++        self.xmlstream = self.authenticator.xmlstream
++
++
++    def test_getFeatures(self):
 +        """
-+        Stub loseConnection because we are a transport.
++        The stream features list SASL with the PLAIN mechanism.
 +        """
-+        self.xmlstream.connectionLost("no reason")
-+
-+
-+    def test_streamStarted(self):
 +        xs = self.xmlstream
 +        xs.dataReceived("<stream:stream xmlns='jabber:client' "
 +                         "xmlns:stream='http://etherx.jabber.org/streams' "
 +
 +        self.assertTrue(xs.headerSent)
 +
-+        # Extract SASL mechanisms
++        # Check SASL mechanisms
 +        features = xs.output[-1]
-+        self.assertEquals(NS_STREAMS, features.uri)
-+        self.assertEquals('features', features.name)
-+        parent = features.elements(NS_XMPP_SASL, 'mechanisms').next()
-+        mechanisms = set()
-+        for child in parent.elements(NS_XMPP_SASL, 'mechanism'):
-+            mechanisms.add(unicode(child))
-+
-+        self.assertIn('PLAIN', mechanisms)
-+
-+
-+    def test_streamStartedWrongNamespace(self):
-+        """
-+        An incorrect stream namespace causes a stream error.
-+        """
-+        xs = self.xmlstream
-+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
-+                         "xmlns:stream='http://etherx.jabber.org/streams' "
-+                         "to='example.org' "
-+                         "version='1.0'>")
-+        streamError = xs.streamErrors[-1]
-+        self.assertEquals('invalid-namespace', streamError.condition)
-+
-+
-+    def test_streamStartedNoTo(self):
-+        """
-+        A missing 'to' attribute on the stream header causes a stream error.
-+        """
-+        xs = self.xmlstream
-+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
-+                         "xmlns:stream='http://etherx.jabber.org/streams' "
-+                         "version='1.0'>")
-+        streamError = xs.streamErrors[-1]
-+        self.assertEquals('improper-addressing', streamError.condition)
-+
-+
-+    def test_streamStartedUnknownHost(self):
-+        """
-+        An unknown 'to' on the stream header causes a stream error.
-+        """
-+        xs = self.xmlstream
-+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
-+                         "xmlns:stream='http://etherx.jabber.org/streams' "
-+                         "to='example.com' "
-+                         "version='1.0'>")
-+        streamError = xs.streamErrors[-1]
-+        self.assertEquals('host-unknown', streamError.condition)
++        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
++                                          "/mechanisms[@xmlns='%s']"
++                                          "/mechanism[@xmlns='%s' and "
++                                                     "text()='PLAIN']" %
++                                          (NS_STREAMS, NS_XMPP_SASL, NS_XMPP_SASL),
++                                      features))
 +
 +
 +    def test_auth(self):
 +                         "to='example.org' "
 +                         "version='1.0'>")
 +        xs.output = []
++        response = b64encode('\x00'.join(['', 'test', 'secret']))
 +        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
-+                         "mechanism='PLAIN'>AHRlc3QAc2VjcmV0</auth>")
-+        self.assertTrue(client.IAccount.providedBy(self.xmlstream.avatar))
++                         "mechanism='PLAIN'>%s</auth>" % response)
++        self.assertTrue(iwokkel.IUserSession.providedBy(self.xmlstream.avatar))
++        self.assertFalse(xs.headerSent)
++        self.assertEqual(1, len(xs.output))
++        self.assertFalse(self.authenticator.initialized)
 +
 +
 +    def test_authInvalidMechanism(self):
 +        xs.output = []
 +        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
 +                         "mechanism='unknown'/>")
-+        xpath.matches(("/failure[@xmlns='%s']"
-+                       "/invalid-mechanism[@xmlns='%s']" %
-+                       (NS_XMPP_SASL, NS_XMPP_SASL)),
-+                      xs.output[-1])
++        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
++                                       "/invalid-mechanism[@xmlns='%s']" %
++                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
++                                      xs.output[-1]))
++
++
++    def test_authFail(self):
++        """
++        Authenticating causes an avatar to be set on the authenticator.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++        xs.output = []
++        response = b64encode('\x00'.join(['', 'test', 'bad']))
++        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
++                         "mechanism='PLAIN'>%s</auth>" % response)
++        self.assertIdentical(None, self.xmlstream.avatar)
++        self.assertTrue(xs.headerSent)
++
++        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
++                                       "/not-authorized[@xmlns='%s']" %
++                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
++                                      xs.output[-1]))
++
++        self.assertFalse(self.authenticator.initialized)
++
++
++    def test_authFailMultiple(self):
++        """
++        Authenticating causes an avatar to be set on the authenticator.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++
++        xs.output = []
++        response = b64encode('\x00'.join(['', 'test', 'bad']))
++
++        attempts = self.authenticator.initializer.failureGrace
++        for attempt in xrange(attempts):
++            xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
++                             "mechanism='PLAIN'>%s</auth>" % response)
++            self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
++                                           "/not-authorized[@xmlns='%s']" %
++                                           (NS_XMPP_SASL, NS_XMPP_SASL)),
++                                          xs.output[-1]))
++        self.xmlstream.assertStreamError(self, condition='policy-violation')
++        self.assertFalse(self.authenticator.initialized)
++
++
++    def test_authException(self):
++        """
++        Other authentication exceptions yield temporary-auth-failure.
++        """
++        class Error(Exception):
++            pass
++
++        def login(credentials, mind, *interfaces):
++            raise Error()
++
++        self.portal.login = login
++
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++        xs.output = []
++        response = b64encode('\x00'.join(['', 'test', 'bad']))
++        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
++                         "mechanism='PLAIN'>%s</auth>" % response)
++        self.assertIdentical(None, self.xmlstream.avatar)
++        self.assertTrue(xs.headerSent)
++
++        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
++                                       "/temporary-auth-failure[@xmlns='%s']" %
++                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
++                                      xs.output[-1]))
++        self.assertFalse(self.authenticator.initialized)
++        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
++
++
++    def test_authNonAsciiUsername(self):
++        """
++        Authenticating causes an avatar to be set on the authenticator.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++        xs.output = []
++        response = b64encode('\x00'.join(['', 'test\xa1', 'secret']))
++        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
++                         "mechanism='PLAIN'>%s</auth>" % response)
++        self.assertIdentical(None, self.xmlstream.avatar)
++        self.assertTrue(xs.headerSent)
++
++        self.assertEqual(1, len(xs.output))
++        failure = xs.output[-1]
++        condition = failure.elements().next()
++        self.assertEqual('not-authorized', condition.name)
++
++
++    def test_authAuthorizationIdentifier(self):
++        """
++        Authorization Identifiers are not supported.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++        xs.output = []
++        response = b64encode('\x00'.join(['other', 'test', 'secret']))
++        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
++                         "mechanism='PLAIN'>%s</auth>" % response)
++        self.assertIdentical(None, self.xmlstream.avatar)
++        self.assertTrue(xs.headerSent)
++
++        self.assertEqual(1, len(xs.output))
++        failure = xs.output[-1]
++        condition = failure.elements().next()
++        self.assertEqual('invalid-authz', condition.name)
++
++
++
++class BindReceivingInitializerTest(unittest.TestCase):
++    """
++    Tests for L{client.BindReceivingInitializer}.
++    """
++
++    def setUp(self):
++        def getInitializers(self):
++            self.initializer = client.BindReceivingInitializer('bind',
++                                                               self.xmlstream)
++            return [self.initializer]
++
++        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
++        self.xmlstream = self.authenticator.xmlstream
++        self.xmlstream.avatar = TestSession('example.org', 'test')
++
++
++    def test_getFeatures(self):
++        """
++        The stream features include resource binding.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++
++        features = xs.output[-1]
++        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
++                                          "/bind[@xmlns='%s']" %
++                                          (NS_STREAMS, NS_XMPP_BIND),
++                                      features))
++
++
++    def test_bind(self):
++        """
++        To bind a resource, the avatar is requested one and a JID is returned.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++
++        # This initializer is required.
++        self.assertFalse(self.authenticator.initialized)
++
++        xs.output = []
++        xs.dataReceived("""<iq type='set'>
++                             <bind xmlns='%s'>Home</bind>
++                           </iq>""" % NS_XMPP_BIND)
++
++        self.assertTrue(xs.headerSent, "Unexpected stream restart")
++
++        # In response to the bind request, a result iq and the new stream
++        # features are sent
++        response = xs.output[-2]
++        self.assertTrue(xpath.matches("/iq[@type='result']"
++                                          "/bind[@xmlns='%s']"
++                                          "/jid[@xmlns='%s' and "
++                                               "text()='%s']" %
++                                          (NS_XMPP_BIND,
++                                           NS_XMPP_BIND,
++                                           'test@example.org/Home'),
++                                      response))
++
++        self.assertTrue(self.authenticator.initialized)
++
++
++
++class SessionReceivingInitializerTest(unittest.TestCase):
++    """
++    Tests for L{client.SessionReceivingInitializer}.
++    """
++
++    def setUp(self):
++        def getInitializers(self):
++            self.initializer = client.SessionReceivingInitializer('session',
++                                                                  self.xmlstream)
++            return [self.initializer]
++
++        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
++        self.xmlstream = self.authenticator.xmlstream
++
++
++    def test_getFeatures(self):
++        """
++        The stream features include session establishment.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++
++        features = xs.output[-1]
++        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
++                                          "/session[@xmlns='%s']" %
++                                          (NS_STREAMS, NS_XMPP_SESSION),
++                                      features))
++
++    def test_session(self):
++        """
++        Session establishment is a no-op iq exchange.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++
++        # This initializer is not required.
++        self.assertTrue(self.authenticator.initialized)
++
++        # If resource binding has completed, xs.otherEntity has been set.
++        xs.otherEntity = JID('test@example.org/Home')
++
++        xs.output = []
++        xs.dataReceived("""<iq type='set'>
++                             <session xmlns='%s'/>
++                           </iq>""" % NS_XMPP_SESSION)
++
++        self.assertTrue(xs.headerSent, "Unexpected stream restart")
++
++        # In response to the session request, a result iq and the new stream
++        # features are sent
++        response = xs.output[-2]
++        self.assertTrue(xpath.matches("/iq[@type='result']", response))
++
++
++
++    def test_sessionNoBind(self):
++        """
++        Session establishment requires resource binding being completed.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++
++        # This initializer is not required.
++        self.assertTrue(self.authenticator.initialized)
++
++        xs.output = []
++        xs.dataReceived("""<iq type='set'>
++                             <session xmlns='%s'/>
++                           </iq>""" % NS_XMPP_SESSION)
++
++        self.assertTrue(xs.headerSent, "Unexpected stream restart")
++
++        # In response to the session request, a result iq and the new stream
++        # features are sent
++        response = xs.output[-2]
++        stanzaError = error.exceptionFromStanza(response)
++        self.assertEqual('forbidden', stanzaError.condition)
++
++
++
++class XMPPClientListenAuthenticatorTest(unittest.TestCase):
++    """
++    Tests for L{client.XMPPClientListenAuthenticator}.
++    """
++
++    def setUp(self):
++        portals = {JID('example.org'): None}
++        self.authenticator = client.XMPPClientListenAuthenticator(portals)
++        self.xmlstream = TestableXmlStream(self.authenticator)
++        self.xmlstream.makeConnection(self)
++
++
++    def test_getInitializersStart(self):
++        """
++        Upon the start of negotation, only the SASL initializer is available.
++        """
++        inits = self.authenticator.getInitializers()
++        (init,) = inits
++        self.assertEqual('sasl', init.name)
++        self.assertIsInstance(init, client.SASLReceivingInitializer)
++
++
++    def test_getInitializersPostSASL(self):
++        """
++        After SASL, the resource binding and session establishment initializers
++        are available.
++        """
++        self.authenticator.completedInitializers = ['sasl']
++        inits = self.authenticator.getInitializers()
++        (bind, session) = inits
++        self.assertEqual('bind', bind.name)
++        self.assertIsInstance(bind, client.BindReceivingInitializer)
++        self.assertEqual('session', session.name)
++        self.assertIsInstance(session, client.SessionReceivingInitializer)
++
++
++    def test_streamStartedWrongNamespace(self):
++        """
++        An incorrect stream namespace causes a stream error.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.org' "
++                         "version='1.0'>")
++        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
++
++
++    def test_streamStartedNoTo(self):
++        """
++        A missing 'to' attribute on the stream header causes a stream error.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "version='1.0'>")
++        self.xmlstream.assertStreamError(self, condition='improper-addressing')
++
++
++    def test_streamStartedUnknownHost(self):
++        """
++        An unknown 'to' on the stream header causes a stream error.
++        """
++        xs = self.xmlstream
++        xs.dataReceived("<stream:stream xmlns='jabber:client' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "to='example.com' "
++                         "version='1.0'>")
++        self.xmlstream.assertStreamError(self, condition='host-unknown')
+diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
+--- a/wokkel/test/test_generic.py
++++ b/wokkel/test/test_generic.py
+@@ -331,15 +331,12 @@
+     """
+ 
+     def setUp(self):
+-        self.gotAuthenticated = False
+-        self.initFailure = None
++        self.gotAuthenticated = 0
+         self.authenticator = generic.FeatureListenAuthenticator()
+         self.authenticator.namespace = 'jabber:server'
+         self.xmlstream = generic.TestableXmlStream(self.authenticator)
+         self.xmlstream.addObserver('//event/stream/authd',
+                                    self.onAuthenticated)
+-        self.xmlstream.addObserver('//event/xmpp/initfailed',
+-                                   self.onInitFailed)
+ 
+         self.init = TestableReceivingInitializer('init', self.xmlstream,
+                                                  'testns', 'test')
+@@ -351,11 +348,7 @@
+ 
+ 
+     def onAuthenticated(self, obj):
+-        self.gotAuthenticated = True
+-
+-
+-    def onInitFailed(self, failure):
+-        self.initFailure = failure
++        self.gotAuthenticated += 1
+ 
+ 
+     def test_getInitializers(self):
+@@ -537,6 +530,38 @@
+                         "  <query xmlns='jabber:iq:version'/>"
+                         "</iq>")
+ 
++    def test_streamStartedInitializerNotRequired(self):
++        """
++        If no initializers are required, initialization is done.
++        """
++        self.init.required = False
++        xs = self.xmlstream
++        xs.makeConnection(proto_helpers.StringTransport())
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "from='example.com' to='example.org' id='12345' "
++                         "version='1.0'>")
++
++        self.assertEqual(1, self.gotAuthenticated)
++
++
++    def test_streamStartedInitializerNotRequiredDoneOnce(self):
++        """
++        If no initializers are required, the authd event is not sent again.
++        """
++        self.init.required = False
++        xs = self.xmlstream
++        xs.makeConnection(proto_helpers.StringTransport())
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "from='example.com' to='example.org' id='12345' "
++                         "version='1.0'>")
++
++        self.assertEqual(1, self.gotAuthenticated)
++        xs.output = []
++        self.init.deferred.callback(None)
++        self.assertEqual(1, self.gotAuthenticated)
++
+ 
+     def test_streamStartedXmlStanzasHandledIgnored(self):
+         """

File listening-authenticator-stream-features.patch

 # HG changeset patch
-# Parent 8b17590769db3088336f1fa65710c48e5ad5dcc1
+# Parent 9393ad83138bbe6ab1c5249a6a115b8e56144622
 Add FeatureListeningAuthenticator.
 
 This new authenticator is for incoming streams and uses initializers
 for stream negotiation, similar to inializers for clients.
 
-TODO:
-
- * Add docstrings.
- * Add support for stream restarts.
-
 diff --git a/wokkel/generic.py b/wokkel/generic.py
 --- a/wokkel/generic.py
 +++ b/wokkel/generic.py
+@@ -10,13 +10,13 @@
+ from zope.interface import implements
+ 
+ from twisted.internet import defer, protocol
+-from twisted.python import reflect
++from twisted.python import log, reflect
+ from twisted.words.protocols.jabber import error, jid, xmlstream
+ from twisted.words.protocols.jabber.xmlstream import toResponse
+ from twisted.words.xish import domish, utility
+ from twisted.words.xish.xmlstream import BootstrapMixin
+ 
+-from wokkel.iwokkel import IDisco
++from wokkel.iwokkel import IDisco, IReceivingInitializer
+ from wokkel.subprotocols import XMPPHandler
+ 
+ IQ_GET = '/iq[@type="get"]'
 @@ -25,6 +25,8 @@
  NS_VERSION = 'jabber:iq:version'
  VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
  def parseXml(string):
      """
      Parse serialized XML into a DOM structure.
-@@ -327,3 +329,116 @@
+@@ -327,3 +329,287 @@
  
      def clientConnectionFailed(self, connector, reason):
          self.deferred.errback(reason)
 +
 +
 +class TestableXmlStream(xmlstream.XmlStream):
++    """
++    XML Stream that buffers outgoing data and catches special events.
++
++    This implementation overrides relevant methods to prevent any data
++    to be sent out on a transport. Instead it buffers all outgoing stanzas,
++    sets flags instead of sending the stream header and footer and logs stream
++    errors so it can be caught by a logging observer.
++
++    @ivar output: Sequence of objects sent out using L{send}. Usually these are
++        L{domish.Element} instances.
++    @type output: C{list}
++
++    @ivar headerSent: Flag set when a stream header would have been sent. When
++        a stream restart occurs through L{reset}, this flag is reset as well.
++    @type headerSent: C{bool}
++
++    @ivar footerSent: Flag set when a stream footer would have been sent
++        explicitly. Note that it is not set when a stream error is sent
++        using L{sendStreamError}.
++    @type footerSent: C{bool}
++    """
++
 +
 +    def __init__(self, authenticator):
 +        xmlstream.XmlStream.__init__(self, authenticator)
 +        self.headerSent = False
 +        self.footerSent = False
-+        self.streamError = None
 +        self.output = []
 +
 +
 +
 +
 +    def sendStreamError(self, streamError):
-+        self.streamError = streamError
++        """
++        Log a stream error.
++
++        If this is called from a Twisted Trial test case, the stream error
++        will be observed by the Trial logging observer. If it is not explicitly
++        tested for (i.e. flushed), this will cause the test case to
++        automatically fail. See L{assertStreamError} for a convenience method
++        to test for stream errors.
++
++        @type streamError: L{error.StreamError}
++        """
++        log.err(streamError)
++
++
++    @staticmethod
++    def assertStreamError(testcase, condition=None, exc=None):
++        """
++        Check if a stream error was sent out.
++
++        To check for stream errors sent out by L{sendStreamError}, this method
++        will flush logged stream errors and inspect the last one. If
++        C{condition} was passed, the logged error is asserted to match that
++        condition. If C{exc} was passed, the logged error is asserted to be
++        identical to it.
++
++        Note that this is takes the calling test case as the first argument, to
++        be able to hook into its methods for flushing errors and making
++        assertions.
++
++        @param testcase: The test case instance that is calling this method.
++        @type testcase: {twisted.trial.unittest.TestCase}
++
++        @param condition: The optional stream error condition to match against.
++        @type condition: C{unicode}.
++
++        @param exc: The optional stream error to check identity against.
++        @type exc: L{error.StreamError}
++        """
++
++        loggedErrors = testcase.flushLoggedErrors(error.StreamError)
++        testcase.assertTrue(loggedErrors, "No stream error was sent")
++        streamError = loggedErrors[-1].value
++        if condition:
++            testcase.assertEqual(condition, streamError.condition)
++        elif exc:
++            testcase.assertIdentical(exc, streamError)
 +
 +
 +    def send(self, obj):
++        """
++        Buffer all outgoing stanzas.
++
++        @type obj: L{domish.Element}
++        """
 +        self.output.append(obj)
 +
 +
 +
++class BaseReceivingInitializer(object):
++    """
++    Base stream initializer for receiving entities.
++    """
++    implements(IReceivingInitializer)
++
++    required = False
++
++    def __init__(self, name, xs):
++        self.name = name
++        self.xmlstream = xs
++        self.deferred = defer.Deferred()
++
++
++    def getFeatures(self):
++        raise NotImplementedError()
++
++
++    def initialize(self):
++        return self.deferred
++
++
++
 +class FeatureListenAuthenticator(xmlstream.ListenAuthenticator):
 +    """
 +    Authenticator for receiving entities with support for initializers.
 +        self.xmlstream.sendStreamError(exc)
 +
 +
-+    def _initializeStream(self):
-+        def cb(result):
-+            result, index = result
-+            self.completedInitializers.append(self._initializers[index])
-+            del self._initializers[index]
++    def connectionMade(self):
++        """
++        Called when the connection has been made.
++
++        Adds an observer to reject XML Stanzas until stream feature negotiation
++        has completed.
++        """
++        xmlstream.ListenAuthenticator.connectionMade(self)
++        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
++
++
++    def _cbInit(self, result):
++        """
++        Mark the initializer as completed and continue to the next.
++        """
++        result, index = result
++        self.completedInitializers.append(self._initializers[index].name)
++        del self._initializers[index]
++
++        if result is xmlstream.Reset:
++            # The initializer initiated a stream restart, bail.
++            return
++        else:
 +            self._initializeStream()
 +
++
++    def _ebInit(self, failure):
++        """
++        Called when an initializer raises an exception.
++
++        If the exception is a L{error.StreamError} it is sent out, otherwise
++        the error is logged and a stream error with condition
++        C{'internal-server-error'} is sent out instead.
++        """
++        firstError = failure.value
++        subFailure = firstError.subFailure
++        if subFailure.check(error.StreamError):
++            exc = subFailure.value
++        else:
++            log.err(subFailure, index=firstError.index)
++            exc = error.StreamError('internal-server-error')
++
++        self.xmlstream.sendStreamError(exc)
++
++
++    def _initializeStream(self):
++        """
++        Initialize the stream.
++
++        This walks all initializers to retrieve their features and determine
++        if there is at least one required initializer. If not, the stream is
++        ready for the exchange of stanzas. The features are sent out and each
++        initializer will have its C{initialize} method called.
++
++        If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is
++        dispatched and the observer rejecting incoming stanzas is removed.
++        """
 +        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
 +        ds = []
 +        required = False
 +                features.addChild(feature)
 +            d = initializer.initialize()
 +            ds.append(d)
++
 +        self.xmlstream.send(features)
++
 +        if not required:
++            # There are no required initializers anymore. This stream is
++            # now ready for the exchange of stanzas.
 +            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
 +            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
++
 +        if ds:
-+            d = defer.DeferredList(ds, fireOnOneCallback=True)
-+            d.addCallback(cb)
++            d = defer.DeferredList(ds, fireOnOneCallback=True,
++                                       fireOnOneErrback=True,
++                                       consumeErrors=True)
++            d.addCallbacks(self._cbInit, self._ebInit)
++
 +
 +    def getInitializers(self):
-+        return []
++        """
++        Get the initializers for the current stage of stream negotiation.
++
++        This will be called at the start of each stream start to retrieve
++        the initializers for which the features are advertised and initiated.
++
++        @rtype: C{list} of C{IReceivingInitializer} instances.
++        """
++        raise NotImplementedError()
 +
 +
 +    def checkStream(self):
-+        # check namespace
++        """
++        Check the stream before sending out a stream header and initialization.
++
++        This is the place to inspect the stream properties and raise a relevant
++        L{error.StreamError} if needed.
++        """
++        # Check stream namespace
 +        if self.xmlstream.namespace != self.namespace:
 +            self.xmlstream.namespace = self.namespace
 +            raise error.StreamError('invalid-namespace')
 +
 +
 +    def streamStarted(self, rootElement):
++        """
++        Called when the stream header has been received.
++
++        Check the stream properties through L{checkStream}, send out
++        a stream header, and retrieve and initialize the stream initializers.
++        """
 +        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
 +
 +        try:
 +            self.xmlstream.sendStreamError(exc)
 +            return
 +
-+        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
 +        self.xmlstream.sendHeader()
 +
 +        self._initializers = self.getInitializers()
 +        self._initializeStream()
+diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
+--- a/wokkel/iwokkel.py
++++ b/wokkel/iwokkel.py
+@@ -11,7 +11,7 @@
+            'IPubSubClient', 'IPubSubService', 'IPubSubResource',
+            'IMUCClient', 'IMUCStatuses']
+ 
+-from zope.interface import Interface
++from zope.interface import Attribute, Interface
+ from twisted.python.deprecate import deprecatedModuleAttribute
+ from twisted.python.versions import Version
+ from twisted.words.protocols.jabber.ijabber import IXMPPHandler
+@@ -982,3 +982,50 @@
+         """
+         Return the number of status conditions.
+         """
++
++
++
++class IReceivingInitializer(Interface):
++    """
++    Interface for XMPP stream initializers for receiving entities.
++    """
++
++    required = Attribute(
++        """
++        This initializer is required to complete feature negotiation.
++        """)
++    name = Attribute(
++        """
++        Identifier for this initializer.
++
++        This identifier is included in
++        L{wokkel.generic.FeatureListenAuthenticator} when an initializer has
++        completed.
++        """)
++    xmlstream = Attribute(
++        """
++        The XML Stream.
++        """)
++    deferred = Attribute(
++        """
++        The deferred returned from initialize.
++        """)
++
++
++    def getFeatures():
++        """
++        Get stream features for this initializer.
++
++        @rtype: C{list} of L{twisted.words.xish.domish.Element}
++        """
++
++
++    def initialize():
++        """
++        Initialize the initializer.
++
++        This is where observers for feature negotiation are set up. When
++        the returned deferred fires, it is assumed to have completed.
++
++        @rtype: L{twisted.internet.defer.Deferred}
++        """
 diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
 --- a/wokkel/test/test_generic.py
 +++ b/wokkel/test/test_generic.py
-@@ -5,9 +5,12 @@
+@@ -5,11 +5,16 @@
  Tests for L{wokkel.generic}.
  """
  
++from zope.interface import verify
++
 +from twisted.internet import defer
 +from twisted.test import proto_helpers
  from twisted.trial import unittest
  from twisted.words.protocols.jabber.jid import JID
 +from twisted.words.protocols.jabber import error, xmlstream
  
- from wokkel import generic
+-from wokkel import generic
++from wokkel import generic, iwokkel
  from wokkel.test.helpers import XmlStreamStub
-@@ -268,3 +271,218 @@
+ 
+ NS_VERSION = 'jabber:iq:version'
+@@ -268,3 +273,331 @@
          The default is no timeout.
          """
          self.assertIdentical(None, self.request.timeout)
 +
 +
 +
-+class TestableReceivingInitializer(object):
++class BaseReceivingInitializerTest(unittest.TestCase):
++    """
++    Tests for L{generic.BaseReceivingInitializer}.
++    """
++
++    def setUp(self):
++        self.init = generic.BaseReceivingInitializer('init', None)
++
++
++    def test_interface(self):
++        verify.verifyObject(iwokkel.IReceivingInitializer, self.init)
++
++
++    def test_getFeatures(self):
++        self.assertRaises(NotImplementedError, self.init.getFeatures)
++
++
++    def test_initialize(self):
++        d = self.init.initialize()
++        self.init.deferred.callback(None)
++        return d
++
++
++
++class TestableReceivingInitializer(generic.BaseReceivingInitializer):
 +    """
 +    Testable initializer for receiving entities.
 +
 +    initialization for this initializer.
 +
 +    @ivar uri: Namespace of the stream feature.
-+    @ivar name: Element localname for the stream feature.
++    @ivar localname: Element localname for the stream feature.
 +    """
 +    required = True
 +
-+    def __init__(self, xs, uri, name):
-+        self.xmlstream = xs
++    def __init__(self, name, xs, uri, localname):
++        generic.BaseReceivingInitializer.__init__(self, name, xs)
 +        self.uri = uri
-+        self.name = name
++        self.localname = localname
 +        self.deferred = defer.Deferred()
 +
 +
 +    def getFeatures(self):
-+        return [domish.Element((self.uri, self.name))]
++        return [domish.Element((self.uri, self.localname))]
 +
 +
-+    def initialize(self):
-+        return self.deferred
 +
 +class FeatureListenAuthenticatorTest(unittest.TestCase):
 +    """
 +        self.xmlstream.addObserver('//event/xmpp/initfailed',
 +                                   self.onInitFailed)
 +
-+        self.init = TestableReceivingInitializer(self.xmlstream, 'testns', 'test')
++        self.init = TestableReceivingInitializer('init', self.xmlstream,
++                                                 'testns', 'test')
 +
 +        def getInitializers():
 +            return [self.init]
 +        self.initFailure = failure
 +
 +
++    def test_getInitializers(self):
++        """
++        Unoverridden getInitializers raises NotImplementedError.
++        """
++        authenticator = generic.FeatureListenAuthenticator()
++        self.assertRaises(
++            NotImplementedError,
++            authenticator.getInitializers)
++
++
 +    def test_streamStarted(self):
 +        """
 +        Upon stream start, stream initializers are set up.
 +        hen, each of the returned initializers will be called to set
 +        themselves up.
 +        """
-+
 +        xs = self.xmlstream
 +        xs.makeConnection(proto_helpers.StringTransport())
 +        xs.dataReceived("<stream:stream xmlns='jabber:server' "
 +        self.assertTrue(self.gotAuthenticated)
 +
 +
++    def test_streamStartedStreamError(self):
++        """
++        A stream error raised by the initializer is sent out.
++        """
++        xs = self.xmlstream
++        xs.makeConnection(proto_helpers.StringTransport())
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "from='example.com' to='example.org' id='12345' "
++                         "version='1.0'>")
++
++        self.assertTrue(xs.headerSent)
++
++        xs.output = []
++        exc = error.StreamError('policy-violation')
++        self.init.deferred.errback(exc)
++
++        self.xmlstream.assertStreamError(self, exc=exc)
++        self.assertFalse(xs.output)
++        self.assertFalse(self.gotAuthenticated)
++
++
++    def test_streamStartedOtherError(self):
++        """
++        Initializer exceptions are logged and yield a internal-server-error.
++        """
++        xs = self.xmlstream
++        xs.makeConnection(proto_helpers.StringTransport())
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "from='example.com' to='example.org' id='12345' "
++                         "version='1.0'>")
++
++        self.assertTrue(xs.headerSent)
++
++        xs.output = []
++        class Error(Exception):
++            pass
++        self.init.deferred.errback(Error())
++
++        self.xmlstream.assertStreamError(self, condition='internal-server-error')
++        self.assertFalse(xs.output)
++        self.assertFalse(self.gotAuthenticated)
++        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
++
++
 +    def test_streamStartedInitializerCompleted(self):
 +        """
 +        Succesfully finished initializers are recorded.
 +                         "from='example.com' to='example.org' id='12345' "
 +                         "version='1.0'>")
 +
++        xs.output = []
 +        self.init.deferred.callback(None)
-+        self.assertEqual([self.init], self.authenticator.completedInitializers)
++        self.assertEqual(['init'], self.authenticator.completedInitializers)
++
++
++    def test_streamStartedInitializerCompletedFeatures(self):
++        """
++        After completing an initializer, stream features are sent again.
++
++        In this case, with only one initializer, there are no more features.
++        """
++        xs = self.xmlstream
++        xs.makeConnection(proto_helpers.StringTransport())
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "from='example.com' to='example.org' id='12345' "
++                         "version='1.0'>")
++
++        xs.output = []
++        self.init.deferred.callback(None)
++
++        self.assertEqual(1, len(xs.output))
++        features = xs.output[-1]
++        self.assertEqual('features', features.name)
++        self.assertEqual(xmlstream.NS_STREAMS, features.uri)
++        self.assertFalse(features.children)
++
++
++    def test_streamStartedInitializerCompletedReset(self):
++        """
++        If an initializer completes with Reset, no features are sent.
++        """
++        xs = self.xmlstream
++        xs.makeConnection(proto_helpers.StringTransport())
++        xs.dataReceived("<stream:stream xmlns='jabber:server' "
++                         "xmlns:stream='http://etherx.jabber.org/streams' "
++                         "from='example.com' to='example.org' id='12345' "
++                         "version='1.0'>")
++
++        xs.output = []
++        self.init.deferred.callback(xmlstream.Reset)
++
++        self.assertEqual(0, len(xs.output))
 +
 +
 +    def test_streamStartedXmlStanzasRejected(self):
 +                        "  <query xmlns='jabber:iq:version'/>"
 +                        "</iq>")
 +
-+        self.assertEqual('not-authorized', xs.streamError.condition)
++        self.xmlstream.assertStreamError(self, condition='not-authorized')
 +
 +
 +    def test_streamStartedCompleteXmlStanzasAllowed(self):
 +                        "  <query xmlns='jabber:iq:version'/>"
 +                        "</iq>")
 +
-+        self.assertIdentical(None, xs.streamError)
-+
 +
 +    def test_streamStartedXmlStanzasHandledIgnored(self):
 +        """
 +        iq.handled = True
 +        xs.dispatch(iq)
 +
-+        self.assertIdentical(None, xs.streamError)
-+
 +
 +    def test_streamStartedNonXmlStanzasIgnored(self):
 +        """
 +
 +        xs.dataReceived("<test xmlns='myns'/>")
 +
-+        self.assertIdentical(None, xs.streamError)
-+
 +
 +    def test_streamStartedCheckStream(self):
 +        """
 +                         "from='example.com' to='example.org' id='12345' "
 +                         "version='1.0'>")
 +
-+        self.assertEqual('undefined-condition', xs.streamError.condition)
++        self.xmlstream.assertStreamError(self, condition='undefined-condition')
 +        self.assertFalse(xs.headerSent)
 +
 +
 +                         "from='example.com' to='example.org' id='12345' "
 +                         "version='1.0'>")
 +
-+        self.assertEqual('invalid-namespace', xs.streamError.condition)
++        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
 roster_server.patch #+c2s
 router_unknown.patch #+c2s
+listening-authenticator-stream-features.patch #+c2s
 
-listening-authenticator-stream-features.patch #+c2s
 client_listen_authenticator.patch #+c2s
 
-c2s_server_factory.patch #+c2s-broken
-session_manager.patch #+c2s-broken
-c2s_stanza_handlers.patch #+c2s-broken
+c2s_server_factory.patch #+c2s
+session_manager.patch #+c2s
+c2s_stanza_handlers.patch #+c2s
 
 version.patch
 

File session_manager.patch

 # HG changeset patch
-# Parent 4e25d6deb8beeb732cadb38349ee820f0dc98b3a
+# Parent 4962500d52ed0bd4a71047c93818303accea55fd
 
-diff -r 4e25d6deb8be wokkel/client.py
---- a/wokkel/client.py	Wed Nov 30 09:32:01 2011 +0100
-+++ b/wokkel/client.py	Wed Nov 30 09:33:11 2011 +0100
-@@ -13,15 +13,16 @@
+diff --git a/wokkel/client.py b/wokkel/client.py
+--- a/wokkel/client.py
++++ b/wokkel/client.py
+@@ -12,18 +12,21 @@
+ 
  import base64
  
++from zope.interface import implements
++
  from twisted.application import service
--from twisted.internet import reactor
+-from twisted.cred import credentials, error as ecred
++from twisted.cred import credentials, error as ecred, portal
+ from twisted.internet import defer, reactor
 -from twisted.python import log
-+from twisted.internet import defer, reactor
 +from twisted.python import log, randbytes
  from twisted.names.srvconnect import SRVConnector
  from twisted.words.protocols.jabber import client, error, sasl, xmlstream
  
  from wokkel import generic
  from wokkel.compat import XmlStreamServerFactory
+ from wokkel.iwokkel import IUserSession
 -from wokkel.subprotocols import StreamManager
 +from wokkel.subprotocols import StreamManager, XMPPHandler
  
  NS_CLIENT = 'jabber:client'
  
-@@ -443,3 +444,105 @@
-         else:
-             raise RecipientUnavailable(u"There is no connection for %s" %
-                                        recipient.full())
+@@ -501,3 +504,161 @@
+ 
+     def onError(self, reason):
+         log.err(reason, "Stream Error")
 +
 +
-+class Session(object):
++
++class UserSession(object):
++
++    implements(IUserSession)
++
++    realm = None
++    mind = None
++
++    connected = False
++    interested = False
++    presence = None
++
++    clientStream = None
++
 +    def __init__(self, entity):
 +        self.entity = entity
++
++
++    def loggedIn(self, realm, mind):
++        self.realm = realm
++        self.mind = mind
++
++
++    def bindResource(self, resource):
++        def cb(entity):
++            self.entity = entity
++            self.connected = True
++            return entity
++
++        d = self.realm.bindResource(self, resource)
++        d.addCallback(cb)
++        return d
++
++
++    def logout(self):
 +        self.connected = False
-+        self.interested = False
-+        self.presence = None
++        self.realm.unbindResource(self)
++
++
++    def send(self, element):