Changeset 65:736d81819863 in ralphm-patches


Ignore:
Timestamp:
Aug 19, 2012, 11:19:55 PM (8 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Fold c2s cred patch, rework authenticator with initializers.

This also marks some later patches as broken (using a different guard),
as they still depend on the previous architecture.

Files:
1 deleted
3 edited

Legend:

Unmodified
Added
Removed
  • client_listen_authenticator.patch

    r57 r65  
    11# HG changeset patch
    2 # Parent 75701188facc278f61a0dfb4bcfcd2232ee771ca
     2# Parent ad0f4165244b1c661023f03d8f7557fe5352337f
    33Add authenticator for accepting XMPP client connections.
    44
     
    77SASL PLAIN mechanism only.
    88
    9 This authenticator needs a backend service to hold the domain served and
    10 relay established connections. Upon binding a resource, its `bindResource`
    11 method is called with the local-part, domain and resource bound to the new
    12 stream.
     9This authenticator needs at least one Twisted Cred portal to hold the
     10domain served. After authenticating, an avatar and a logout callback are
     11returned. Upon binding a resource, the avatar's `bindResource` method is
     12called with the desired resource name. Upon stream disconnect, the
     13logout callback is called.
    1314
    1415TODO:
     
    1617 * Add tests.
    1718 * Add docstrings.
    18  * Readd stream namespace check?
    19  * Host checks.
    20  * Password checks.
    21  * Support for multiple domains?
    2219
    23 diff -r 75701188facc wokkel/client.py
    24 --- a/wokkel/client.py  Wed Nov 23 09:52:41 2011 +0100
    25 +++ b/wokkel/client.py  Wed Nov 30 09:31:07 2011 +0100
    26 @@ -10,14 +10,26 @@
     20diff --git a/wokkel/client.py b/wokkel/client.py
     21--- a/wokkel/client.py
     22+++ b/wokkel/client.py
     23@@ -10,14 +10,28 @@
    2724 that should probably eventually move there.
    2825 """
     
    3027+import base64
    3128+
     29+from zope.interface import Interface
     30+
    3231 from twisted.application import service
    33  from twisted.internet import reactor
     32-from twisted.internet import reactor
     33+from twisted.cred import credentials, error as ecred
     34+from twisted.internet import defer, reactor
    3435+from twisted.python import log
    3536 from twisted.names.srvconnect import SRVConnector
     
    4344+NS_CLIENT = 'jabber:client'
    4445+
    45 +XPATH_ALL = "/*"
    4646+XPATH_AUTH = "/auth[@xmlns='%s']" % sasl.NS_XMPP_SASL
    4747+XPATH_BIND = "/iq[@type='set']/bind[@xmlns='%s']" % client.NS_XMPP_BIND
     
    5252     """
    5353     Check what authentication methods are available.
    54 @@ -51,7 +63,7 @@
     54@@ -51,7 +65,7 @@
    5555     autentication.
    5656     """
     
    6161     def __init__(self, jid, password):
    6262         xmlstream.ConnectAuthenticator.__init__(self, jid.host)
    63 @@ -186,3 +198,152 @@
     63@@ -186,3 +200,210 @@
    6464     c = XMPPClientConnector(reactor, domain, factory)
    6565     c.connect()
     
    6868+
    6969+
    70 +class XMPPClientListenAuthenticator(xmlstream.ListenAuthenticator):
    71 +    namespace = NS_CLIENT
    72 +
    73 +    def __init__(self, service):
    74 +        self.service = service
     70+class InvalidMechanism(Exception):
     71+    """
     72+    The requested SASL mechanism is invalid.
     73+    """
     74+
     75+
     76+class IAccount(Interface):
     77+    pass
     78+
     79+
     80+
     81+class SASLReceivingInitializer(object):
     82+    required = True
     83+
     84+    def __init__(self, xs, portal):
     85+        self.xmlstream = xs
     86+        self.portal = portal
     87+        self.deferred = defer.Deferred()
    7588+        self.failureGrace = 3
    76 +        self.state = 'auth'
    77 +
    78 +
    79 +    def associateWithStream(self, xs):
    80 +        xmlstream.ListenAuthenticator.associateWithStream(self, xs)
    81 +        self.xmlstream.addObserver(XPATH_ALL, self.onElementFallback, -1)
    82 +
    83 +
    84 +    def onElementFallback(self, element):
    85 +        if element.handled:
    86 +            return
    87 +
    88 +        exc = error.StreamError('not-authorized')
    89 +        self.xmlstream.sendStreamError(exc)
    90 +
    91 +
    92 +    def streamStarted(self, rootElement):
    93 +        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
    94 +
    95 +        # check namespace
    96 +        #if self.xmlstream.namespace != self.namespace:
    97 +        #    self.xmlstream.namespace = self.namespace
    98 +        #    exc = error.StreamError('invalid-namespace')
    99 +        #    self.xmlstream.sendStreamError(exc)
    100 +        #    return
    101 +
    102 +        # TODO: check domain (self.service.domain)
    103 +
    104 +        self.xmlstream.sendHeader()
    105 +
    106 +        try:
    107 +            stateHandlerName = 'streamStarted_' + self.state
    108 +            stateHandler = getattr(self, stateHandlerName)
    109 +        except AttributeError:
    110 +            log.msg('streamStarted handler for', self.state, 'not found')
    111 +        else:
    112 +            stateHandler()
    113 +
    114 +
    115 +    def toState(self, state):
    116 +        self.state = state
    117 +        if state == 'initialized':
    118 +            self.xmlstream.removeObserver(XPATH_ALL, self.onElementFallback)
    119 +            self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1)
    120 +            self.xmlstream.dispatch(self.xmlstream,
    121 +                                    xmlstream.STREAM_AUTHD_EVENT)
    122 +
    123 +
    124 +    def streamStarted_auth(self):
    125 +        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
    126 +        features.addElement((sasl.NS_XMPP_SASL, 'mechanisms'))
    127 +        features.mechanisms.addElement('mechanism', content='PLAIN')
    128 +        self.xmlstream.send(features)
    129 +        self.xmlstream.addOnetimeObserver(XPATH_AUTH, self.onAuth)
     89+
     90+
     91+    def getFeatures(self):
     92+        feature = domish.Element((sasl.NS_XMPP_SASL, 'mechanisms'))
     93+        feature.addElement('mechanism', content='PLAIN')
     94+        return [feature]
     95+
     96+
     97+    def initialize(self):
     98+        self.xmlstream.addObserver(XPATH_AUTH, self.onAuth)
     99+        return self.deferred
    130100+
    131101+
     
    133103+        auth.handled = True
    134104+
    135 +        if auth.getAttribute('mechanism') != 'PLAIN':
    136 +            failure = domish.Element((sasl.NS_XMPP_SASL, 'failure'))
    137 +            failure.addElement('invalid-mechanism')
    138 +            self.xmlstream.send(failure)
     105+        def cb(_):
     106+            response = domish.Element((sasl.NS_XMPP_SASL, 'success'))
     107+            self.xmlstream.send(response)
     108+            self.xmlstream.reset()
     109+
     110+        def eb(failure):
     111+            if failure.check(ecred.UnauthorizedLogin):
     112+                condition = 'not-authorized'
     113+            elif failure.check(InvalidMechanism):
     114+                condition = 'invalid-mechanism'
     115+            else:
     116+                log.err(failure)
     117+                condition = 'temporary-auth-failure'
     118+
     119+            response = domish.Element((sasl.NS_XMPP_SASL, 'failure'))
     120+            response.addElement(condition)
     121+            self.xmlstream.send(response)
    139122+
    140123+            # Close stream on too many failing authentication attempts
    141124+            self.failureGrace -= 1
    142125+            if self.failureGrace == 0:
    143 +                self.xmlstream.sendFooter()
    144 +            else:
    145 +                self.xmlstream.addOnetimeObserver(XPATH_AUTH, self.onAuth)
    146 +
    147 +            return
    148 +
     126+                raise error.StreamError('policy-violation')
     127+                #self.xmlstream.sendStreamError(exc)
     128+
     129+        d = defer.maybeDeferred(self.doAuth, auth)
     130+        d.addCallbacks(cb, eb)
     131+        d.chainDeferred(self.deferred)
     132+
     133+
     134+    def credentialsFromPlain(self, auth):
    149135+        initialResponse = base64.b64decode(unicode(auth))
    150136+        authzid, authcid, passwd = initialResponse.split('\x00')
    151137+
    152 +        # TODO: check passwd
    153 +
    154 +        # authenticated
    155 +
    156 +        self.username = authcid
    157 +
    158 +        success = domish.Element((sasl.NS_XMPP_SASL, 'success'))
    159 +        self.xmlstream.send(success)
    160 +        self.xmlstream.reset()
    161 +
    162 +        self.toState('bind')
    163 +
    164 +
    165 +    def streamStarted_bind(self):
    166 +        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
    167 +        features.addElement((client.NS_XMPP_BIND, 'bind'))
    168 +        features.addElement((client.NS_XMPP_SESSION, 'session'))
    169 +        self.xmlstream.send(features)
     138+        # FIXME: bail if authzid is set
     139+
     140+        creds = credentials.UsernamePassword(username=authcid.encode('utf-8'),
     141+                                             password=passwd)
     142+        return creds
     143+
     144+
     145+    def doAuth(self, auth):
     146+
     147+        if auth.getAttribute('mechanism') != 'PLAIN':
     148+            raise InvalidMechanism()
     149+
     150+        creds = self.credentialsFromPlain(auth)
     151+
     152+        def cb((iface, avatar, logout)):
     153+            self.xmlstream.avatar = avatar
     154+            self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT,
     155+                                       lambda _: logout())
     156+
     157+        d = self.portal.login(creds, None, IAccount)
     158+        d.addCallback(cb)
     159+        return d
     160+
     161+
     162+
     163+class BindReceivingInitializer(object):
     164+    required = True
     165+
     166+    def __init__(self, xs):
     167+        self.xmlstream = xs
     168+        self.deferred = defer.Deferred()
     169+
     170+
     171+    def getFeatures(self):
     172+        feature = domish.Element((client.NS_XMPP_BIND, 'bind'))
     173+        return [feature]
     174+
     175+
     176+    def initialize(self):
    170177+        self.xmlstream.addOnetimeObserver(XPATH_BIND, self.onBind)
     178+        return self.deferred
    171179+
    172180+
     
    174182+        def cb(boundJID):
    175183+            self.xmlstream.otherEntity = boundJID
    176 +            self.toState('initialized')
    177184+
    178185+            response = xmlstream.toResponse(iq, 'result')
     
    194201+        iq.handled = True
    195202+        resource = unicode(iq.bind) or None
    196 +        d = self.service.bindResource(self.username,
    197 +                                      self.service.domain,
    198 +                                      resource)
     203+        d = self.xmlstream.avatar.bindResource(resource)
    199204+        d.addCallback(cb)
    200205+        d.addErrback(eb)
    201206+        d.addCallback(self.xmlstream.send)
     207+        d.chainDeferred(self.deferred)
     208+
     209+
     210+
     211+class SessionReceivingInitializer(object):
     212+    required = False
     213+
     214+    def __init__(self, xs):
     215+        self.xmlstream = xs
     216+        self.deferred = defer.Deferred()
     217+
     218+
     219+    def getFeatures(self):
     220+        feature = domish.Element((client.NS_XMPP_SESSION, 'session'))
     221+        return [feature]
     222+
     223+
     224+    def initialize(self):
     225+        self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1)
     226+        return self.deferred
    202227+
    203228+
     
    206231+
    207232+        reply = domish.Element((None, 'iq'))
    208 +        reply['type'] = 'result'
    209 +        if iq.getAttribute('id'):
    210 +            reply['id'] = iq['id']
    211 +        reply.addElement((client.NS_XMPP_SESSION, 'session'))
     233+
     234+        if self.xmlstream.otherEntity:
     235+            reply = xmlstream.toResponse(iq)
     236+        else:
     237+            reply = error.StanzaError('forbidden').toResponse(iq)
    212238+        self.xmlstream.send(reply)
    213 +
    214 +
    215 +
     239+        self.deferred.callback(None)
     240+
     241+
     242+
     243+class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator):
     244+    namespace = NS_CLIENT
     245+
     246+    def __init__(self, portals):
     247+        generic.FeatureListenAuthenticator.__init__(self)
     248+        self.portals = portals
     249+        self.portal = None
     250+
     251+
     252+    def getInitializers(self):
     253+        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)]
     259+        else:
     260+            return []
     261+
     262+
     263+    def checkStream(self):
     264+        generic.FeatureListenAuthenticator.checkStream(self)
     265+
     266+        if not self.xmlstream.thisEntity:
     267+            raise error.StreamError('improper-addressing')
     268+
     269+        # Check if we serve the domain and use the associated portal.
     270+        try:
     271+            self.portal = self.portals[self.xmlstream.thisEntity]
     272+        except KeyError:
     273+            raise error.StreamError('host-unknown')
     274diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
     275--- a/wokkel/test/test_client.py
     276+++ b/wokkel/test/test_client.py
     277@@ -5,14 +5,21 @@
     278 Tests for L{wokkel.client}.
     279 """
     280 
     281+from zope.interface import implements
     282+
     283+from twisted.cred.portal import IRealm, Portal
     284+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
     285 from twisted.internet import defer
     286 from twisted.trial import unittest
     287 from twisted.words.protocols.jabber import xmlstream
     288 from twisted.words.protocols.jabber.client import XMPPAuthenticator
     289 from twisted.words.protocols.jabber.jid import JID
     290+from twisted.words.protocols.jabber.sasl import NS_XMPP_SASL
     291+from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
     292+from twisted.words.protocols.jabber.xmlstream import NS_STREAMS
     293 from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT
     294-from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
     295 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
     296+from twisted.words.xish import xpath
     297 
     298 from wokkel import client
     299 
     300@@ -155,3 +162,167 @@
     301         self.assertEqual(factory.deferred, d2)
     302 
     303         return d1
     304+
     305+
     306+class TestAccount(object):
     307+    implements(client.IAccount)
     308+
     309+
     310+class TestRealm(object):
     311+
     312+    implements(IRealm)
     313+
     314+    logoutCalled = False
     315+
     316+    def requestAvatar(self, avatarId, mind, *interfaces):
     317+        return (client.IAccount, TestAccount(), self.logout)
     318+
     319+
     320+    def logout(self):
     321+        self.logoutCalled = True
     322+
     323+
     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)
     353+
     354+
     355+class XMPPClientListenAuthenticatorTest(unittest.TestCase):
     356+    """
     357+    Tests for L{client.XMPPClientListenAuthenticator}.
     358+    """
     359+
     360+    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}
     366+        self.authenticator = client.XMPPClientListenAuthenticator(portals)
     367+        self.xmlstream = TestableXmlStream(self.authenticator)
     368+        self.xmlstream.makeConnection(self)
     369+
     370+
     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)
     397+
     398+
     399+    def test_streamStartedWrongNamespace(self):
     400+        """
     401+        An incorrect stream namespace causes a stream error.
     402+        """
     403+        xs = self.xmlstream
     404+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     405+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     406+                         "to='example.org' "
     407+                         "version='1.0'>")
     408+        streamError = xs.streamErrors[-1]
     409+        self.assertEquals('invalid-namespace', streamError.condition)
     410+
     411+
     412+    def test_streamStartedNoTo(self):
     413+        """
     414+        A missing 'to' attribute on the stream header causes a stream error.
     415+        """
     416+        xs = self.xmlstream
     417+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     418+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     419+                         "version='1.0'>")
     420+        streamError = xs.streamErrors[-1]
     421+        self.assertEquals('improper-addressing', streamError.condition)
     422+
     423+
     424+    def test_streamStartedUnknownHost(self):
     425+        """
     426+        An unknown 'to' on the stream header causes a stream error.
     427+        """
     428+        xs = self.xmlstream
     429+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     430+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     431+                         "to='example.com' "
     432+                         "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])
  • listening-authenticator-stream-features.patch

    r64 r65  
    11# HG changeset patch
    2 # Parent 4f4ccf34fee8357fdf4fbf1a741e4c5113553d5d
     2# Parent 8b17590769db3088336f1fa65710c48e5ad5dcc1
     3Add FeatureListeningAuthenticator.
     4
     5This new authenticator is for incoming streams and uses initializers
     6for stream negotiation, similar to inializers for clients.
     7
     8TODO:
     9
     10 * Add docstrings.
     11 * Add support for stream restarts.
    312
    413diff --git a/wokkel/generic.py b/wokkel/generic.py
    514--- a/wokkel/generic.py
    615+++ b/wokkel/generic.py
    7 @@ -327,3 +327,74 @@
     16@@ -25,6 +25,8 @@
     17 NS_VERSION = 'jabber:iq:version'
     18 VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
     19 
     20+XPATH_ALL = "/*"
     21+
     22 def parseXml(string):
     23     """
     24     Parse serialized XML into a DOM structure.
     25@@ -327,3 +329,116 @@
    826 
    927     def clientConnectionFailed(self, connector, reason):
     
    1836+        self.headerSent = False
    1937+        self.footerSent = False
    20 +        self.streamErrors = []
     38+        self.streamError = None
    2139+        self.output = []
    2240+
     
    3654+
    3755+    def sendStreamError(self, streamError):
    38 +        self.streamErrors.append(streamError)
     56+        self.streamError = streamError
    3957+
    4058+
     
    4866+    Authenticator for receiving entities with support for initializers.
    4967+    """
     68+
     69+    def __init__(self):
     70+        self.completedInitializers = []
     71+
     72+
     73+    def _onElementFallback(self, element):
     74+        """
     75+        Fallback observer that rejects XML Stanzas.
     76+
     77+        This observer is active while stream feature negotiation has not yet
     78+        completed.
     79+        """
     80+        # ignore elements that are not XML Stanzas
     81+        if (element.uri not in (self.namespace) or
     82+            element.name not in ('iq', 'message', 'presence')):
     83+            return
     84+
     85+        # ignore elements that have already been handled
     86+        if element.handled:
     87+            return
     88+
     89+        exc = error.StreamError('not-authorized')
     90+        self.xmlstream.sendStreamError(exc)
     91+
    5092+
    5193+    def _initializeStream(self):
    5294+        def cb(result):
    5395+            result, index = result
    54 +            del self.initializers[index]
     96+            self.completedInitializers.append(self._initializers[index])
     97+            del self._initializers[index]
    5598+            self._initializeStream()
    5699+
     
    58101+        ds = []
    59102+        required = False
    60 +        for initializer in self.initializers:
     103+        for initializer in self._initializers:
    61104+            required = required or initializer.required
    62105+            for feature in initializer.getFeatures():
     
    66109+        self.xmlstream.send(features)
    67110+        if not required:
     111+            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
    68112+            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
    69113+        if ds:
     
    71115+            d.addCallback(cb)
    72116+
     117+    def getInitializers(self):
     118+        return []
     119+
     120+
     121+    def checkStream(self):
     122+        # check namespace
     123+        if self.xmlstream.namespace != self.namespace:
     124+            self.xmlstream.namespace = self.namespace
     125+            raise error.StreamError('invalid-namespace')
    73126+
    74127+
    75128+    def streamStarted(self, rootElement):
    76129+        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
    77 +        # check headers?
     130+
     131+        try:
     132+            self.checkStream()
     133+        except error.StreamError, exc:
     134+            self.xmlstream.sendStreamError(exc)
     135+            return
     136+
     137+        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
    78138+        self.xmlstream.sendHeader()
    79 +        self.initializers = self.getInitializers()
     139+
     140+        self._initializers = self.getInitializers()
    80141+        self._initializeStream()
    81 +
    82142diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    83143--- a/wokkel/test/test_generic.py
     
    92152 from twisted.words.xish import domish
    93153 from twisted.words.protocols.jabber.jid import JID
    94 +from twisted.words.protocols.jabber import xmlstream
     154+from twisted.words.protocols.jabber import error, xmlstream
    95155 
    96156 from wokkel import generic
    97157 from wokkel.test.helpers import XmlStreamStub
    98 @@ -268,3 +271,79 @@
     158@@ -268,3 +271,218 @@
    99159         The default is no timeout.
    100160         """
     
    102162+
    103163+
     164+
     165+class TestableReceivingInitializer(object):
     166+    """
     167+    Testable initializer for receiving entities.
     168+
     169+    This initializer advertises support for a stream feature denoted by
     170+    C{uri} and C{name}. Its C{deferred} should be fired to complete
     171+    initialization for this initializer.
     172+
     173+    @ivar uri: Namespace of the stream feature.
     174+    @ivar name: Element localname for the stream feature.
     175+    """
     176+    required = True
     177+
     178+    def __init__(self, xs, uri, name):
     179+        self.xmlstream = xs
     180+        self.uri = uri
     181+        self.name = name
     182+        self.deferred = defer.Deferred()
     183+
     184+
     185+    def getFeatures(self):
     186+        return [domish.Element((self.uri, self.name))]
     187+
     188+
     189+    def initialize(self):
     190+        return self.deferred
    104191+
    105192+class FeatureListenAuthenticatorTest(unittest.TestCase):
     
    112199+        self.initFailure = None
    113200+        self.authenticator = generic.FeatureListenAuthenticator()
     201+        self.authenticator.namespace = 'jabber:server'
    114202+        self.xmlstream = generic.TestableXmlStream(self.authenticator)
    115203+        self.xmlstream.addObserver('//event/stream/authd',
     
    118206+                                   self.onInitFailed)
    119207+
     208+        self.init = TestableReceivingInitializer(self.xmlstream, 'testns', 'test')
     209+
     210+        def getInitializers():
     211+            return [self.init]
     212+
     213+        self.authenticator.getInitializers = getInitializers
     214+
    120215+
    121216+    def onAuthenticated(self, obj):
     
    133228+        The method getInitializers will determine the available stream
    134229+        initializers given the current state of stream initialization.
    135 +        Then, each of the returned initializers will be called to set
     230+        hen, each of the returned initializers will be called to set
    136231+        themselves up.
    137232+        """
    138 +        class Initializer(object):
    139 +            required = True
    140 +            def __init__(self, xs):
    141 +                self.xmlstream = xs
    142 +                self.deferred = defer.Deferred()
    143 +            def getFeatures(self):
    144 +                return [domish.Element(('testns', 'test'))]
    145 +            def initialize(self):
    146 +                return self.deferred
    147 +
    148 +        init = Initializer(self.xmlstream)
    149 +
    150 +        def getInitializers():
    151 +            return [init]
    152 +
    153 +        self.authenticator.getInitializers = getInitializers
    154233+
    155234+        xs = self.xmlstream
     
    164243+        # Check if features were sent
    165244+        features = xs.output[-1]
    166 +        print features.toXml()
    167245+        self.assertEquals(xmlstream.NS_STREAMS, features.uri)
    168246+        self.assertEquals('features', features.name)
    169247+        feature = features.elements().next()
    170 +        print feature.toXml()
    171248+        self.assertEqual('testns', feature.uri)
    172249+        self.assertEqual('test', feature.name)
     
    174251+        self.assertFalse(self.gotAuthenticated)
    175252+
    176 +        init.deferred.callback(None)
     253+        self.init.deferred.callback(None)
    177254+        self.assertTrue(self.gotAuthenticated)
     255+
     256+
     257+    def test_streamStartedInitializerCompleted(self):
     258+        """
     259+        Succesfully finished initializers are recorded.
     260+        """
     261+        xs = self.xmlstream
     262+        xs.makeConnection(proto_helpers.StringTransport())
     263+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     264+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     265+                         "from='example.com' to='example.org' id='12345' "
     266+                         "version='1.0'>")
     267+
     268+        self.init.deferred.callback(None)
     269+        self.assertEqual([self.init], self.authenticator.completedInitializers)
     270+
     271+
     272+    def test_streamStartedXmlStanzasRejected(self):
     273+        """
     274+        XML Stanzas may not be sent before feature negotiation has completed.
     275+        """
     276+        xs = self.xmlstream
     277+        xs.makeConnection(proto_helpers.StringTransport())
     278+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     279+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     280+                         "from='example.com' to='example.org' id='12345' "
     281+                         "version='1.0'>")
     282+
     283+        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     284+                        "  <query xmlns='jabber:iq:version'/>"
     285+                        "</iq>")
     286+
     287+        self.assertEqual('not-authorized', xs.streamError.condition)
     288+
     289+
     290+    def test_streamStartedCompleteXmlStanzasAllowed(self):
     291+        """
     292+        XML Stanzas may sent after feature negotiation has completed.
     293+        """
     294+        xs = self.xmlstream
     295+        xs.makeConnection(proto_helpers.StringTransport())
     296+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     297+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     298+                         "from='example.com' to='example.org' id='12345' "
     299+                         "version='1.0'>")
     300+
     301+        self.init.deferred.callback(None)
     302+
     303+        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     304+                        "  <query xmlns='jabber:iq:version'/>"
     305+                        "</iq>")
     306+
     307+        self.assertIdentical(None, xs.streamError)
     308+
     309+
     310+    def test_streamStartedXmlStanzasHandledIgnored(self):
     311+        """
     312+        XML Stanzas that have already been handled are ignored.
     313+        """
     314+        xs = self.xmlstream
     315+        xs.makeConnection(proto_helpers.StringTransport())
     316+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     317+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     318+                         "from='example.com' to='example.org' id='12345' "
     319+                         "version='1.0'>")
     320+
     321+        iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>"
     322+                              "  <query xmlns='jabber:iq:version'/>"
     323+                              "</iq>")
     324+        iq.handled = True
     325+        xs.dispatch(iq)
     326+
     327+        self.assertIdentical(None, xs.streamError)
     328+
     329+
     330+    def test_streamStartedNonXmlStanzasIgnored(self):
     331+        """
     332+        Elements that are not XML Stranzas are not rejected.
     333+        """
     334+        xs = self.xmlstream
     335+        xs.makeConnection(proto_helpers.StringTransport())
     336+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     337+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     338+                         "from='example.com' to='example.org' id='12345' "
     339+                         "version='1.0'>")
     340+
     341+        xs.dataReceived("<test xmlns='myns'/>")
     342+
     343+        self.assertIdentical(None, xs.streamError)
     344+
     345+
     346+    def test_streamStartedCheckStream(self):
     347+        """
     348+        Stream errors raised by checkStream are sent out.
     349+        """
     350+        def checkStream():
     351+            raise error.StreamError('undefined-condition')
     352+
     353+        self.authenticator.checkStream = checkStream
     354+        xs = self.xmlstream
     355+        xs.makeConnection(proto_helpers.StringTransport())
     356+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     357+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     358+                         "from='example.com' to='example.org' id='12345' "
     359+                         "version='1.0'>")
     360+
     361+        self.assertEqual('undefined-condition', xs.streamError.condition)
     362+        self.assertFalse(xs.headerSent)
     363+
     364+
     365+    def test_checkStreamNamespace(self):
     366+        """
     367+        The stream namespace must match the pre-defined stream namespace.
     368+        """
     369+        xs = self.xmlstream
     370+        xs.makeConnection(proto_helpers.StringTransport())
     371+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     372+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     373+                         "from='example.com' to='example.org' id='12345' "
     374+                         "version='1.0'>")
     375+
     376+        self.assertEqual('invalid-namespace', xs.streamError.condition)
  • series

    r64 r65  
    11roster_server.patch #+c2s
    22router_unknown.patch #+c2s
     3
     4listening-authenticator-stream-features.patch #+c2s
    35client_listen_authenticator.patch #+c2s
    4 c2s-cred.patch #+c2s
    5 listening-authenticator-stream-features.patch
    66
    7 c2s_server_factory.patch #+c2s
    8 session_manager.patch #+c2s
    9 c2s_stanza_handlers.patch #+c2s
     7c2s_server_factory.patch #+c2s-broken
     8session_manager.patch #+c2s-broken
     9c2s_stanza_handlers.patch #+c2s-broken
    1010
    1111version.patch
Note: See TracChangeset for help on using the changeset viewer.