source: ralphm-patches/listening-authenticator-stream-features.patch @ 65:736d81819863

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

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.

File size: 12.3 KB
  • wokkel/generic.py

    # HG changeset patch
    # Parent 8b17590769db3088336f1fa65710c48e5ad5dcc1
    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 b  
    2525NS_VERSION = 'jabber:iq:version'
    2626VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
    2727
     28XPATH_ALL = "/*"
     29
    2830def parseXml(string):
    2931    """
    3032    Parse serialized XML into a DOM structure.
     
    327329
    328330    def clientConnectionFailed(self, connector, reason):
    329331        self.deferred.errback(reason)
     332
     333
     334
     335class TestableXmlStream(xmlstream.XmlStream):
     336
     337    def __init__(self, authenticator):
     338        xmlstream.XmlStream.__init__(self, authenticator)
     339        self.headerSent = False
     340        self.footerSent = False
     341        self.streamError = None
     342        self.output = []
     343
     344
     345    def reset(self):
     346        xmlstream.XmlStream.reset(self)
     347        self.headerSent = False
     348
     349
     350    def sendHeader(self):
     351        self.headerSent = True
     352
     353
     354    def sendFooter(self):
     355        self.footerSent = True
     356
     357
     358    def sendStreamError(self, streamError):
     359        self.streamError = streamError
     360
     361
     362    def send(self, obj):
     363        self.output.append(obj)
     364
     365
     366
     367class FeatureListenAuthenticator(xmlstream.ListenAuthenticator):
     368    """
     369    Authenticator for receiving entities with support for initializers.
     370    """
     371
     372    def __init__(self):
     373        self.completedInitializers = []
     374
     375
     376    def _onElementFallback(self, element):
     377        """
     378        Fallback observer that rejects XML Stanzas.
     379
     380        This observer is active while stream feature negotiation has not yet
     381        completed.
     382        """
     383        # ignore elements that are not XML Stanzas
     384        if (element.uri not in (self.namespace) or
     385            element.name not in ('iq', 'message', 'presence')):
     386            return
     387
     388        # ignore elements that have already been handled
     389        if element.handled:
     390            return
     391
     392        exc = error.StreamError('not-authorized')
     393        self.xmlstream.sendStreamError(exc)
     394
     395
     396    def _initializeStream(self):
     397        def cb(result):
     398            result, index = result
     399            self.completedInitializers.append(self._initializers[index])
     400            del self._initializers[index]
     401            self._initializeStream()
     402
     403        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
     404        ds = []
     405        required = False
     406        for initializer in self._initializers:
     407            required = required or initializer.required
     408            for feature in initializer.getFeatures():
     409                features.addChild(feature)
     410            d = initializer.initialize()
     411            ds.append(d)
     412        self.xmlstream.send(features)
     413        if not required:
     414            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
     415            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
     416        if ds:
     417            d = defer.DeferredList(ds, fireOnOneCallback=True)
     418            d.addCallback(cb)
     419
     420    def getInitializers(self):
     421        return []
     422
     423
     424    def checkStream(self):
     425        # check namespace
     426        if self.xmlstream.namespace != self.namespace:
     427            self.xmlstream.namespace = self.namespace
     428            raise error.StreamError('invalid-namespace')
     429
     430
     431    def streamStarted(self, rootElement):
     432        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
     433
     434        try:
     435            self.checkStream()
     436        except error.StreamError, exc:
     437            self.xmlstream.sendStreamError(exc)
     438            return
     439
     440        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
     441        self.xmlstream.sendHeader()
     442
     443        self._initializers = self.getInitializers()
     444        self._initializeStream()
  • wokkel/test/test_generic.py

    diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    a b  
    55Tests for L{wokkel.generic}.
    66"""
    77
     8from twisted.internet import defer
     9from twisted.test import proto_helpers
    810from twisted.trial import unittest
    911from twisted.words.xish import domish
    1012from twisted.words.protocols.jabber.jid import JID
     13from twisted.words.protocols.jabber import error, xmlstream
    1114
    1215from wokkel import generic
    1316from wokkel.test.helpers import XmlStreamStub
     
    268271        The default is no timeout.
    269272        """
    270273        self.assertIdentical(None, self.request.timeout)
     274
     275
     276
     277class TestableReceivingInitializer(object):
     278    """
     279    Testable initializer for receiving entities.
     280
     281    This initializer advertises support for a stream feature denoted by
     282    C{uri} and C{name}. Its C{deferred} should be fired to complete
     283    initialization for this initializer.
     284
     285    @ivar uri: Namespace of the stream feature.
     286    @ivar name: Element localname for the stream feature.
     287    """
     288    required = True
     289
     290    def __init__(self, xs, uri, name):
     291        self.xmlstream = xs
     292        self.uri = uri
     293        self.name = name
     294        self.deferred = defer.Deferred()
     295
     296
     297    def getFeatures(self):
     298        return [domish.Element((self.uri, self.name))]
     299
     300
     301    def initialize(self):
     302        return self.deferred
     303
     304class FeatureListenAuthenticatorTest(unittest.TestCase):
     305    """
     306    Tests for L{generic.FeatureListenAuthenticator}.
     307    """
     308
     309    def setUp(self):
     310        self.gotAuthenticated = False
     311        self.initFailure = None
     312        self.authenticator = generic.FeatureListenAuthenticator()
     313        self.authenticator.namespace = 'jabber:server'
     314        self.xmlstream = generic.TestableXmlStream(self.authenticator)
     315        self.xmlstream.addObserver('//event/stream/authd',
     316                                   self.onAuthenticated)
     317        self.xmlstream.addObserver('//event/xmpp/initfailed',
     318                                   self.onInitFailed)
     319
     320        self.init = TestableReceivingInitializer(self.xmlstream, 'testns', 'test')
     321
     322        def getInitializers():
     323            return [self.init]
     324
     325        self.authenticator.getInitializers = getInitializers
     326
     327
     328    def onAuthenticated(self, obj):
     329        self.gotAuthenticated = True
     330
     331
     332    def onInitFailed(self, failure):
     333        self.initFailure = failure
     334
     335
     336    def test_streamStarted(self):
     337        """
     338        Upon stream start, stream initializers are set up.
     339
     340        The method getInitializers will determine the available stream
     341        initializers given the current state of stream initialization.
     342        hen, each of the returned initializers will be called to set
     343        themselves up.
     344        """
     345
     346        xs = self.xmlstream
     347        xs.makeConnection(proto_helpers.StringTransport())
     348        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     349                         "xmlns:stream='http://etherx.jabber.org/streams' "
     350                         "from='example.com' to='example.org' id='12345' "
     351                         "version='1.0'>")
     352
     353        self.assertTrue(xs.headerSent)
     354
     355        # Check if features were sent
     356        features = xs.output[-1]
     357        self.assertEquals(xmlstream.NS_STREAMS, features.uri)
     358        self.assertEquals('features', features.name)
     359        feature = features.elements().next()
     360        self.assertEqual('testns', feature.uri)
     361        self.assertEqual('test', feature.name)
     362
     363        self.assertFalse(self.gotAuthenticated)
     364
     365        self.init.deferred.callback(None)
     366        self.assertTrue(self.gotAuthenticated)
     367
     368
     369    def test_streamStartedInitializerCompleted(self):
     370        """
     371        Succesfully finished initializers are recorded.
     372        """
     373        xs = self.xmlstream
     374        xs.makeConnection(proto_helpers.StringTransport())
     375        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     376                         "xmlns:stream='http://etherx.jabber.org/streams' "
     377                         "from='example.com' to='example.org' id='12345' "
     378                         "version='1.0'>")
     379
     380        self.init.deferred.callback(None)
     381        self.assertEqual([self.init], self.authenticator.completedInitializers)
     382
     383
     384    def test_streamStartedXmlStanzasRejected(self):
     385        """
     386        XML Stanzas may not be sent before feature negotiation has completed.
     387        """
     388        xs = self.xmlstream
     389        xs.makeConnection(proto_helpers.StringTransport())
     390        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     391                         "xmlns:stream='http://etherx.jabber.org/streams' "
     392                         "from='example.com' to='example.org' id='12345' "
     393                         "version='1.0'>")
     394
     395        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     396                        "  <query xmlns='jabber:iq:version'/>"
     397                        "</iq>")
     398
     399        self.assertEqual('not-authorized', xs.streamError.condition)
     400
     401
     402    def test_streamStartedCompleteXmlStanzasAllowed(self):
     403        """
     404        XML Stanzas may sent after feature negotiation has completed.
     405        """
     406        xs = self.xmlstream
     407        xs.makeConnection(proto_helpers.StringTransport())
     408        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     409                         "xmlns:stream='http://etherx.jabber.org/streams' "
     410                         "from='example.com' to='example.org' id='12345' "
     411                         "version='1.0'>")
     412
     413        self.init.deferred.callback(None)
     414
     415        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     416                        "  <query xmlns='jabber:iq:version'/>"
     417                        "</iq>")
     418
     419        self.assertIdentical(None, xs.streamError)
     420
     421
     422    def test_streamStartedXmlStanzasHandledIgnored(self):
     423        """
     424        XML Stanzas that have already been handled are ignored.
     425        """
     426        xs = self.xmlstream
     427        xs.makeConnection(proto_helpers.StringTransport())
     428        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     429                         "xmlns:stream='http://etherx.jabber.org/streams' "
     430                         "from='example.com' to='example.org' id='12345' "
     431                         "version='1.0'>")
     432
     433        iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>"
     434                              "  <query xmlns='jabber:iq:version'/>"
     435                              "</iq>")
     436        iq.handled = True
     437        xs.dispatch(iq)
     438
     439        self.assertIdentical(None, xs.streamError)
     440
     441
     442    def test_streamStartedNonXmlStanzasIgnored(self):
     443        """
     444        Elements that are not XML Stranzas are not rejected.
     445        """
     446        xs = self.xmlstream
     447        xs.makeConnection(proto_helpers.StringTransport())
     448        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     449                         "xmlns:stream='http://etherx.jabber.org/streams' "
     450                         "from='example.com' to='example.org' id='12345' "
     451                         "version='1.0'>")
     452
     453        xs.dataReceived("<test xmlns='myns'/>")
     454
     455        self.assertIdentical(None, xs.streamError)
     456
     457
     458    def test_streamStartedCheckStream(self):
     459        """
     460        Stream errors raised by checkStream are sent out.
     461        """
     462        def checkStream():
     463            raise error.StreamError('undefined-condition')
     464
     465        self.authenticator.checkStream = checkStream
     466        xs = self.xmlstream
     467        xs.makeConnection(proto_helpers.StringTransport())
     468        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     469                         "xmlns:stream='http://etherx.jabber.org/streams' "
     470                         "from='example.com' to='example.org' id='12345' "
     471                         "version='1.0'>")
     472
     473        self.assertEqual('undefined-condition', xs.streamError.condition)
     474        self.assertFalse(xs.headerSent)
     475
     476
     477    def test_checkStreamNamespace(self):
     478        """
     479        The stream namespace must match the pre-defined stream namespace.
     480        """
     481        xs = self.xmlstream
     482        xs.makeConnection(proto_helpers.StringTransport())
     483        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     484                         "xmlns:stream='http://etherx.jabber.org/streams' "
     485                         "from='example.com' to='example.org' id='12345' "
     486                         "version='1.0'>")
     487
     488        self.assertEqual('invalid-namespace', xs.streamError.condition)
Note: See TracBrowser for help on using the repository browser.