source: ralphm-patches/s2s.patch @ 6:46e82ac33f4a

Last change on this file since 6:46e82ac33f4a was 6:46e82ac33f4a, checked in by Ralph Meijer <ralphm@…>, 12 years ago

Add s2s code.

File size: 27.5 KB
  • new file wokkel/server.py

    diff -r c52801c0d6c2 wokkel/server.py
    - +  
     1# -*- test-case-name: wokkel.test.test_server -*-
     2#
     3# Copyright (c) 2003-2008 Ralph Meijer
     4# See LICENSE for details.
     5
     6"""
     7XMPP Server-to-Server protocol.
     8
     9This module implements several aspects of XMPP server-to-server communications
     10as described in XMPP Core (RFC 3920). Refer to that document for the meaning
     11of the used terminology.
     12"""
     13
     14# hashlib is new in Python 2.5, try that first.
     15try:
     16    from hashlib import sha256
     17except ImportError:
     18    from Crypto.Hash.SHA256 import new as sha256
     19
     20import hmac
     21
     22from zope.interface import implements
     23
     24from twisted.application import service
     25from twisted.internet import defer, reactor
     26from twisted.names.srvconnect import SRVConnector
     27from twisted.python import log
     28from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream
     29from twisted.words.xish import domish
     30
     31from wokkel.generic import DeferredXmlStreamFactory, XmlPipe
     32from wokkel.compat import XmlStreamServerFactory
     33
     34NS_DIALBACK = 'jabber:server:dialback'
     35
     36def generateKey(secret, receivingServer, originatingServer, streamID):
     37    """
     38    Generate a dialback key for server-to-server XMPP Streams.
     39
     40    The dialback key is generated using the algorithm described in
     41    U{XEP-0185<http://www.xmpp.org/extensions/xep-0185.html>}. The used
     42    terminology for the parameters is described in RFC-3920.
     43
     44    @param secret: the shared secret known to the Originating Server and
     45                   Authoritive Server.
     46    @type secret: C{str}
     47    @param receivingServer: the Receiving Server host name.
     48    @type receivingServer: C{str}
     49    @param originatingServer: the Originating Server host name.
     50    @type originatingServer: C{str}
     51    @param streamID: the Stream ID as generated by the Receiving Server.
     52    @type streamID: C{str}
     53    @return: hexadecimal digest of the generated key.
     54    @type: C{str}
     55    """
     56
     57    hashObject = sha256()
     58    hashObject.update(secret)
     59    hashedSecret = hashObject.hexdigest()
     60    message = " ".join([receivingServer, originatingServer, streamID])
     61    hash = hmac.HMAC(hashedSecret, message, digestmod=sha256)
     62    return hash.hexdigest()
     63
     64
     65def trapStreamError(xs, observer):
     66    """
     67    Trap stream errors.
     68
     69    This wraps an observer to catch exceptions. In case of a
     70    L{error.StreamError}, it is send over the given XML stream. All other
     71    exceptions yield a C{'internal-server-error'} stream error, that is
     72    sent over the stream, while the exception is logged.
     73
     74    @return: Wrapped observer
     75    """
     76
     77    def wrappedObserver(element):
     78        try:
     79            observer(element)
     80        except error.StreamError, exc:
     81            xs.sendStreamError(exc)
     82        except:
     83            log.err()
     84            exc = error.StreamError('internal-server-error')
     85            xs.sendStreamError(exc)
     86
     87    return wrappedObserver
     88
     89
     90class XMPPServerConnector(SRVConnector):
     91    def __init__(self, reactor, domain, factory):
     92        SRVConnector.__init__(self, reactor, 'xmpp-server', domain, factory)
     93
     94
     95    def pickServer(self):
     96        host, port = SRVConnector.pickServer(self)
     97
     98        if not self.servers and not self.orderedServers:
     99            # no SRV record, fall back..
     100            port = 5269
     101
     102        return host, port
     103
     104
     105class DialbackFailed(Exception):
     106    pass
     107
     108
     109
     110class OriginatingDialbackInitializer(object):
     111    """
     112    Server Dialback Initializer for the Orginating Server.
     113    """
     114
     115    implements(ijabber.IInitiatingInitializer)
     116
     117    _deferred = None
     118
     119    def __init__(self, xs, thisHost, otherHost, secret):
     120        self.xmlstream = xs
     121        self.thisHost = thisHost
     122        self.otherHost = otherHost
     123        self.secret = secret
     124
     125
     126    def initialize(self):
     127        self._deferred = defer.Deferred()
     128        self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT,
     129                                   self.onStreamError)
     130        self.xmlstream.addObserver("/result[@xmlns='%s']" % NS_DIALBACK,
     131                                   self.onResult)
     132
     133        key = generateKey(self.secret, self.otherHost,
     134                          self.thisHost, self.xmlstream.sid)
     135
     136        result = domish.Element((NS_DIALBACK, 'result'))
     137        result['from'] = self.thisHost
     138        result['to'] = self.otherHost
     139        result.addContent(key)
     140
     141        self.xmlstream.send(result)
     142
     143        return self._deferred
     144
     145
     146    def onResult(self, result):
     147        self.xmlstream.removeObserver(xmlstream.STREAM_ERROR_EVENT,
     148                                      self.onStreamError)
     149        if result['type'] == 'valid':
     150            self.xmlstream.otherEntity = jid.internJID(self.otherHost)
     151            self._deferred.callback(None)
     152        else:
     153            self._deferred.errback(DialbackFailed())
     154
     155
     156    def onStreamError(self, failure):
     157        self.xmlstream.removeObserver("/result[@xmlns='%s']" % NS_DIALBACK,
     158                                      self.onResult)
     159        self._deferred.errback(failure)
     160
     161
     162
     163class ReceivingDialbackInitializer(object):
     164    """
     165    Server Dialback Initializer for the Receiving Server.
     166    """
     167
     168    implements(ijabber.IInitiatingInitializer)
     169
     170    _deferred = None
     171
     172    def __init__(self, xs, thisHost, otherHost, originalStreamID, key):
     173        self.xmlstream = xs
     174        self.thisHost = thisHost
     175        self.otherHost = otherHost
     176        self.originalStreamID = originalStreamID
     177        self.key = key
     178
     179
     180    def initialize(self):
     181        self._deferred = defer.Deferred()
     182        self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT,
     183                                   self.onStreamError)
     184        self.xmlstream.addObserver("/verify[@xmlns='%s']" % NS_DIALBACK,
     185                                   self.onVerify)
     186
     187        verify = domish.Element((NS_DIALBACK, 'verify'))
     188        verify['from'] = self.thisHost
     189        verify['to'] = self.otherHost
     190        verify['id'] = self.originalStreamID
     191        verify.addContent(self.key)
     192
     193        self.xmlstream.send(verify)
     194        return self._deferred
     195
     196
     197    def onVerify(self, verify):
     198        self.xmlstream.removeObserver(xmlstream.STREAM_ERROR_EVENT,
     199                                      self.onStreamError)
     200        if verify['id'] != self.originalStreamID:
     201            self.xmlstream.sendStreamError(error.StreamError('invalid-id'))
     202            self._deferred.errback(DialbackFailed())
     203        elif verify['to'] != self.thisHost:
     204            self.xmlstream.sendStreamError(error.StreamError('host-unknown'))
     205            self._deferred.errback(DialbackFailed())
     206        elif verify['from'] != self.otherHost:
     207            self.xmlstream.sendStreamError(error.StreamError('invalid-from'))
     208            self._deferred.errback(DialbackFailed())
     209        elif verify['type'] == 'valid':
     210            self._deferred.callback(None)
     211        else:
     212            self._deferred.errback(DialbackFailed())
     213
     214
     215    def onStreamError(self, failure):
     216        self.xmlstream.removeObserver("/verify[@xmlns='%s']" % NS_DIALBACK,
     217                                      self.onVerify)
     218        self._deferred.errback(failure)
     219
     220
     221
     222class XMPPServerConnectAuthenticator(xmlstream.ConnectAuthenticator):
     223    namespace = 'jabber:server'
     224
     225    def __init__(self, thisHost, otherHost, secret):
     226        self.thisHost = thisHost
     227        self.otherHost = otherHost
     228        self.secret = secret
     229        xmlstream.ConnectAuthenticator.__init__(self, otherHost)
     230
     231
     232    def connectionMade(self):
     233        self.xmlstream.thisEntity = jid.internJID(self.thisHost)
     234        self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream',
     235                                   NS_DIALBACK: 'db'}
     236        xmlstream.ConnectAuthenticator.connectionMade(self)
     237
     238
     239    def associateWithStream(self, xs):
     240        xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
     241        init = OriginatingDialbackInitializer(xs, self.thisHost,
     242                                              self.otherHost, self.secret)
     243        xs.initializers = [init]
     244
     245
     246
     247class XMPPServerVerifyAuthenticator(xmlstream.ConnectAuthenticator):
     248    namespace = 'jabber:server'
     249
     250    def __init__(self, thisHost, otherHost, originalStreamID, key):
     251        self.thisHost = thisHost
     252        self.otherHost = otherHost
     253        self.originalStreamID = originalStreamID
     254        self.key = key
     255        xmlstream.ConnectAuthenticator.__init__(self, otherHost)
     256
     257
     258    def connectionMade(self):
     259        self.xmlstream.thisEntity = jid.internJID(self.thisHost)
     260        self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream',
     261                                   NS_DIALBACK: 'db'}
     262        xmlstream.ConnectAuthenticator.connectionMade(self)
     263
     264
     265    def associateWithStream(self, xs):
     266        xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
     267        init = ReceivingDialbackInitializer(xs, self.thisHost, self.otherHost,
     268                                            self.originalStreamID, self.key)
     269        xs.initializers = [init]
     270
     271
     272
     273class XMPPServerListenAuthenticator(xmlstream.ListenAuthenticator):
     274    namespace = 'jabber:server'
     275    connectorClass = XMPPServerConnector
     276
     277    def __init__(self, domain, secret):
     278        xmlstream.ListenAuthenticator.__init__(self)
     279        self.domain = domain
     280        self.secret = secret
     281
     282
     283    def streamStarted(self, rootElement):
     284        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
     285
     286        # Compatibility fix for pre-8.2 implementations of ListenAuthenticator
     287        if not self.xmlstream.sid:
     288            from twisted.python import randbytes
     289            self.xmlstream.sid = randbytes.secureRandom(8).encode('hex')
     290
     291        def prepareStream():
     292            self.xmlstream.namespace = self.namespace
     293            self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream',
     294                                       NS_DIALBACK: 'db'}
     295            self.xmlstream.thisEntity = jid.internJID(self.domain)
     296
     297        try:
     298            if xmlstream.NS_STREAMS != rootElement.uri or \
     299               self.namespace != self.xmlstream.namespace or \
     300               ('db', NS_DIALBACK) not in rootElement.localPrefixes.iteritems():
     301                raise error.StreamError('invalid-namespace')
     302
     303            if self.xmlstream.thisEntity.full() != self.domain:
     304                raise error.StreamError('host-unknown')
     305        except error.StreamError, exc:
     306            prepareStream()
     307            self.xmlstream.sendStreamError(exc)
     308            return
     309
     310        self.xmlstream.addObserver("//verify[@xmlns='%s']" % NS_DIALBACK,
     311                                   trapStreamError(self.xmlstream,
     312                                                   self.onVerify))
     313        self.xmlstream.addObserver("//result[@xmlns='%s']" % NS_DIALBACK,
     314                                   self.onResult)
     315
     316        prepareStream()
     317        self.xmlstream.sendHeader()
     318
     319        if self.xmlstream.version >= (1, 0):
     320            features = domish.Element((xmlstream.NS_STREAMS, 'features'))
     321            self.xmlstream.send(features)
     322
     323
     324
     325    def onVerify(self, verify):
     326        try:
     327            receivingServer = jid.JID(verify['from'])
     328            originatingServer = jid.JID(verify['to'])
     329        except (KeyError, jid.InvalidFormat):
     330            raise error.StreamError('improper-addressing')
     331
     332        if originatingServer != self.domain:
     333            raise error.StreamError('host-unknown')
     334
     335        if receivingServer != self.xmlstream.thisEntity:
     336            raise error.StreamError('invalid-from')
     337
     338        streamID = verify.getAttribute('id', '')
     339        key = unicode(verify)
     340
     341        calculatedKey = generateKey(self.secret, receivingServer,
     342                                    originatingServer, streamID)
     343        validity = (key == calculatedKey) and 'valid' or 'invalid'
     344
     345        reply = domish.Element((NS_DIALBACK, 'verify'))
     346        reply['from'] = originatingServer
     347        reply['to'] = receivingServer
     348        reply['id'] = streamID
     349        reply['type'] = validity
     350        self.xmlstream.send(reply)
     351
     352
     353    def onResult(self, result):
     354        def connected(xs):
     355            self.verifyStream = xs
     356
     357            def logDataIn(buf):
     358                log.msg("RECV!: %r" % buf)
     359
     360            def logDataOut(buf):
     361                log.msg("SEND!: %r" % buf)
     362
     363            xs.rawDataInFn = logDataIn
     364            xs.rawDataOutFn = logDataOut
     365
     366        def reply(validity):
     367            factory.stopTrying()
     368            self.verifyStream.transport.loseConnection()
     369            self.verifyStream = None
     370
     371            reply = domish.Element((NS_DIALBACK, 'result'))
     372            reply['from'] = result['to']
     373            reply['to'] = result['from']
     374            reply['type'] = validity
     375            self.xmlstream.send(reply)
     376
     377        def valid(xs):
     378            reply('valid')
     379            self.xmlstream.otherEntity = jid.internJID(originatingServer)
     380            self.xmlstream.dispatch(self.xmlstream,
     381                                    xmlstream.STREAM_AUTHD_EVENT)
     382
     383        def invalid(failure):
     384            reply('invalid')
     385
     386        originatingServer = result['from']
     387
     388        authenticator = XMPPServerVerifyAuthenticator(self.domain,
     389                                                      originatingServer,
     390                                                      self.xmlstream.sid,
     391                                                      unicode(result))
     392        factory = xmlstream.XmlStreamFactory(authenticator)
     393        factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, connected)
     394        factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, valid)
     395        factory.addBootstrap(xmlstream.INIT_FAILED_EVENT, invalid)
     396        connector = self.connectorClass(reactor,
     397                                        originatingServer,
     398                                        factory)
     399        connector.connect()
     400
     401
     402
     403class DeferredS2SClientFactory(DeferredXmlStreamFactory):
     404    """
     405    Deferred firing factory for initiating XMPP server-to-server connection.
     406
     407    The deferred has its callbacks called upon succesful authentication with
     408    the other server. In case of failed authentication or connection, the
     409    deferred will have its errbacks called instead.
     410    """
     411
     412    def __init__(self, domain, otherHost, secret):
     413        authenticator = XMPPServerConnectAuthenticator(domain,
     414                                                            otherHost,
     415                                                            secret)
     416        DeferredXmlStreamFactory.__init__(self, authenticator)
     417
     418
     419
     420def initiateS2S(factory):
     421    domain = factory.authenticator.otherHost
     422    c = XMPPServerConnector(reactor, domain, factory)
     423    c.connect()
     424    return factory.deferred
     425
     426
     427
     428class ServerService(service.Service):
     429    """
     430    Service for managing XMPP server to server connections.
     431    """
     432
     433    logTraffic = False
     434
     435    def __init__(self, router, domain, port=5222):
     436        self.router = router
     437        self.domain = domain
     438        self.port = port
     439        self.secret = 'woei!'
     440
     441        def authenticatorFactory():
     442            return XMPPServerListenAuthenticator(self.domain, self.secret)
     443        self.factory = XmlStreamServerFactory(authenticatorFactory)
     444        self.factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
     445                                  self.makeConnection)
     446        self.factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
     447                                  self.incomingInitialized)
     448
     449        self.incomingStreams = {}
     450        self.outgoingStreams = {}
     451        self.outgoingQueues = {}
     452        self.outgoingConnecting = set()
     453        self.serial = 0
     454
     455
     456    def startService(self):
     457        pipe = XmlPipe()
     458        self.xmlstream = pipe.source
     459        self.router.addRoute(None, pipe.sink)
     460        self.xmlstream.addObserver('/*', self.send)
     461
     462        service.Service.startService(self)
     463        reactor.listenTCP(self.port, self.factory)
     464
     465
     466    def makeConnection(self, xs):
     467        xs.serial = self.serial
     468        self.serial += 1
     469
     470        def logDataIn(buf):
     471            log.msg("RECV (%d): %r" % (xs.serial, buf))
     472
     473        def logDataOut(buf):
     474            log.msg("SEND (%d): %r" % (xs.serial, buf))
     475
     476        if self.logTraffic:
     477            xs.rawDataInFn = logDataIn
     478            xs.rawDataOutFn = logDataOut
     479
     480
     481    def incomingInitialized(self, xs):
     482        self.incomingStreams[xs.otherEntity.host] = xs
     483        xs.addObserver(xmlstream.STREAM_END_EVENT,
     484                       lambda _: self.incomingDisconnected(xs))
     485        xs.addObserver('/*', lambda element: self.onElement(element, xs))
     486
     487
     488    def incomingDisconnected(self, xs):
     489        del self.incomingStreams[xs.otherEntity.host]
     490
     491
     492    def outgoingInitialized(self, xs):
     493        self.outgoingStreams[xs.otherEntity.host] = xs
     494        xs.addObserver(xmlstream.STREAM_END_EVENT,
     495                       lambda _: self.outgoingDisconnected(xs))
     496
     497        if xs.otherEntity.host in self.outgoingQueues:
     498            print "Hier!"
     499            for element in self.outgoingQueues[xs.otherEntity.host]:
     500                xs.send(element)
     501            del self.outgoingQueues[xs.otherEntity.host]
     502
     503
     504    def outgoingDisconnected(self, xs):
     505        del self.outgoingStreams[xs.otherEntity.host]
     506
     507
     508    def initiateOutgoingStream(self, otherHost):
     509        if otherHost in self.outgoingConnecting:
     510            return
     511
     512        factory = DeferredS2SClientFactory(self.domain,
     513                                           otherHost,
     514                                           self.secret)
     515        factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
     516                             self.makeConnection)
     517        factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
     518                             self.outgoingInitialized)
     519
     520        def resetConnecting(_):
     521            self.outgoingConnecting.remove(otherHost)
     522
     523        d = initiateS2S(factory)
     524        def debug(result):
     525            print result
     526            return result
     527        d.addBoth(debug)
     528        d.addBoth(resetConnecting)
     529        self.outgoingConnecting.add(otherHost)
     530
     531
     532    def onElement(self, element, xs):
     533        """
     534        Called when an element was received from one of the connected streams.
     535
     536        """
     537        if element.handled:
     538            return
     539
     540        if jid.internJID(element["from"]).host != xs.otherEntity.host:
     541            xs.sendStreamError(error.StreamError('invalid-from'))
     542        else:
     543            self.xmlstream.send(element)
     544
     545
     546    def send(self, stanza):
     547        """
     548        Send stanza to the proper XML Stream.
     549
     550        This uses addressing embedded in the stanza to find the correct stream
     551        to forward the stanza to.
     552        """
     553
     554        destination = jid.internJID(stanza["to"]).host
     555
     556        if destination not in self.outgoingStreams:
     557            # There is no connection with the destination (yet). Cache the
     558            # outgoing stanza until the connection has been established.
     559            # XXX: If the connection cannot be established, the queue should
     560            #      be emptied at some point.
     561            if destination not in self.outgoingQueues:
     562                self.outgoingQueues[destination] = []
     563            self.outgoingQueues[destination].append(stanza)
     564            self.initiateOutgoingStream(destination)
     565        else:
     566            self.outgoingStreams[destination].send(stanza)
  • new file wokkel/test/test_server.py

    diff -r c52801c0d6c2 wokkel/test/test_server.py
    - +  
     1# Copyright (c) 2003-2008 Ralph Meijer
     2# See LICENSE for details.
     3
     4"""
     5Tests for L{wokkel.server}.
     6"""
     7
     8from twisted.trial import unittest
     9from twisted.words.protocols.jabber import error, xmlstream
     10from twisted.test.proto_helpers import StringTransport
     11
     12from wokkel import server
     13
     14NS_STREAMS = 'http://etherx.jabber.org/streams'
     15DIALBACK_NS = "jabber:server:dialback"
     16
     17class GenerateKeyTest(unittest.TestCase):
     18    """
     19    Tests for L{server.generateKey}.
     20    """
     21
     22    def testBasic(self):
     23        secret = "s3cr3tf0rd14lb4ck"
     24        receiving = "example.net"
     25        originating = "example.com"
     26        id = "D60000229F"
     27
     28        key = server.generateKey(secret, receiving, originating, id)
     29
     30        self.assertEqual(key,
     31            '008c689ff366b50c63d69a3e2d2c0e0e1f8404b0118eb688a0102c87cb691bdc')
     32
     33
     34
     35class XMPPServerListenAuthenticatorTest(unittest.TestCase):
     36    """
     37    Tests for L{server.XMPPServerListenAuthenticator}.
     38    """
     39
     40    receiving = "example.org"
     41    originating = "example.net"
     42    id_ = "2093845023948"
     43    secret = "not-telling"
     44
     45    def setUp(self):
     46        self.output = []
     47        self.authenticator = server.XMPPServerListenAuthenticator(
     48                        self.receiving, self.secret)
     49        self.xmlstream = xmlstream.XmlStream(self.authenticator)
     50        self.xmlstream.send = self.output.append
     51        self.xmlstream.transport = StringTransport()
     52
     53
     54    def test_attributes(self):
     55        """
     56        Test attributes of authenticator and stream objects.
     57        """
     58        self.assertEquals(self.secret, self.authenticator.secret)
     59        self.assertEquals(self.receiving, self.authenticator.domain)
     60        self.assertEquals(self.xmlstream.initiating, False)
     61
     62
     63    def test_streamStartedVersion0(self):
     64        """
     65        The authenticator supports pre-XMPP 1.0 streams.
     66        """
     67        self.xmlstream.connectionMade()
     68        self.xmlstream.dataReceived(
     69            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     70                           "xmlns:db='jabber:server:dialback' "
     71                           "xmlns='jabber:server' "
     72                           "to='example.org'>")
     73        self.assertEqual((0, 0), self.xmlstream.version)
     74
     75
     76    def test_streamStartedVersion1(self):
     77        """
     78        The authenticator supports XMPP 1.0 streams.
     79        """
     80        self.xmlstream.connectionMade()
     81        self.xmlstream.dataReceived(
     82            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     83                           "xmlns:db='jabber:server:dialback' "
     84                           "xmlns='jabber:server' "
     85                           "to='example.org' "
     86                           "version='1.0'>")
     87        self.assertEqual((1, 0), self.xmlstream.version)
     88
     89
     90    def test_streamStartedSID(self):
     91        """
     92        The response stream will have a stream ID.
     93        """
     94        self.xmlstream.connectionMade()
     95        self.assertIdentical(None, self.xmlstream.sid)
     96
     97        self.xmlstream.dataReceived(
     98            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     99                           "xmlns:db='jabber:server:dialback' "
     100                           "xmlns='jabber:server' "
     101                           "to='example.org' "
     102                           "version='1.0'>")
     103        self.assertNotIdentical(None, self.xmlstream.sid)
     104
     105
     106    def test_streamStartedSentResponseHeader(self):
     107        """
     108        A stream header is sent in response to the incoming stream header.
     109        """
     110        self.xmlstream.connectionMade()
     111        self.assertFalse(self.xmlstream._headerSent)
     112
     113        self.xmlstream.dataReceived(
     114            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     115                           "xmlns:db='jabber:server:dialback' "
     116                           "xmlns='jabber:server' "
     117                           "to='example.org'>")
     118        self.assertTrue(self.xmlstream._headerSent)
     119
     120
     121    def test_streamStartedNotSentFeatures(self):
     122        """
     123        No features are sent in response to an XMPP < 1.0 stream header.
     124        """
     125        self.xmlstream.connectionMade()
     126        self.xmlstream.dataReceived(
     127            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     128                           "xmlns:db='jabber:server:dialback' "
     129                           "xmlns='jabber:server' "
     130                           "to='example.org'>")
     131        self.assertEqual(1, len(self.output))
     132
     133
     134    def test_streamStartedSentFeatures(self):
     135        """
     136        Features are sent in response to an XMPP >= 1.0 stream header.
     137        """
     138        self.xmlstream.connectionMade()
     139        self.xmlstream.dataReceived(
     140            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     141                           "xmlns:db='jabber:server:dialback' "
     142                           "xmlns='jabber:server' "
     143                           "to='example.org' "
     144                           "version='1.0'>")
     145        self.assertEqual(2, len(self.output))
     146        features = self.output[-1]
     147        self.assertEqual(NS_STREAMS, features.uri)
     148        self.assertEqual('features', features.name)
     149
     150
     151    def test_streamRootElement(self):
     152        """
     153        Test stream error on wrong stream namespace.
     154        """
     155        self.xmlstream.connectionMade()
     156        self.xmlstream.dataReceived(
     157            "<stream:stream xmlns:stream='badns' "
     158                           "xmlns:db='jabber:server:dialback' "
     159                           "xmlns='jabber:server' "
     160                           "to='example.org'>")
     161
     162        self.assertEquals(3, len(self.output))
     163        exc = error.exceptionFromStreamError(self.output[1])
     164        self.assertEquals('invalid-namespace', exc.condition)
     165
     166
     167    def test_streamDefaultNamespace(self):
     168        """
     169        Test stream error on missing dialback namespace.
     170        """
     171        self.xmlstream.connectionMade()
     172        self.xmlstream.dataReceived(
     173            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     174                           "xmlns:db='jabber:server:dialback' "
     175                           "xmlns='badns' "
     176                           "to='example.org'>")
     177
     178        self.assertEquals(3, len(self.output))
     179        exc = error.exceptionFromStreamError(self.output[1])
     180        self.assertEquals('invalid-namespace', exc.condition)
     181
     182
     183    def test_streamNoDialbackNamespace(self):
     184        """
     185        Test stream error on missing dialback namespace.
     186        """
     187        self.xmlstream.connectionMade()
     188        self.xmlstream.dataReceived(
     189            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     190                           "xmlns='jabber:server' "
     191                           "to='example.org'>")
     192
     193        self.assertEquals(3, len(self.output))
     194        exc = error.exceptionFromStreamError(self.output[1])
     195        self.assertEquals('invalid-namespace', exc.condition)
     196
     197
     198    def test_streamBadDialbackNamespace(self):
     199        """
     200        Test stream error on missing dialback namespace.
     201        """
     202        self.xmlstream.connectionMade()
     203        self.xmlstream.dataReceived(
     204            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     205                           "xmlns:db='badns' "
     206                           "xmlns='jabber:server' "
     207                           "to='example.org'>")
     208
     209        self.assertEquals(3, len(self.output))
     210        exc = error.exceptionFromStreamError(self.output[1])
     211        self.assertEquals('invalid-namespace', exc.condition)
     212
     213
     214    def test_streamToUnknownHost(self):
     215        """
     216        Test stream error on stream's to attribute having unknown host.
     217        """
     218        self.xmlstream.connectionMade()
     219        self.xmlstream.dataReceived(
     220            "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
     221                           "xmlns:db='jabber:server:dialback' "
     222                           "xmlns='jabber:server' "
     223                           "to='badhost'>")
     224
     225        self.assertEquals(3, len(self.output))
     226        exc = error.exceptionFromStreamError(self.output[1])
     227        self.assertEquals('host-unknown', exc.condition)
Note: See TracBrowser for help on using the repository browser.