Changeset 72:727b4d29c48e in ralphm-patches for c2s_server_factory.patch


Ignore:
Timestamp:
Jan 27, 2013, 10:40:32 PM (8 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Major reworking of avatars, session manager and stanza handlers.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • c2s_server_factory.patch

    r71 r72  
    11# HG changeset patch
    2 # Parent 8cb185c88211ac69faa924f6e93425d102daa2bc
     2# Parent 49294b2cf829414b42141731b5130d91474c0443
    33Add factory for accepting client connections.
    44
    5 The new XMPPC2SServerFactory is a server factory for accepting client
     5The new `XMPPC2SServerFactory` is a server factory for accepting client
    66connections. It uses `XMPPClientListenAuthenticator` to perform the
    7 steps for authentication and binding of a resource, and keeps a list
    8 of all streams.
    9 
    10 Upon loss of the connection, the service is called with `unbindResource`.
    11 Received stanzas cause the service's `onElement` to be called.
    12 
    13 The factory has a `deliverStanza` method to deliver stanzas to a particular
    14 recipient. This is used for stanzas that have different recipient addressing
    15 than the actual recipient (for presence and messages from a different (or no)
    16 resource).
     7steps for authentication and binding of a resource.
     8
     9For each connection, the factory also sets up subprotocol handlers by
     10calling `setupHandlers`. By default these are `RecipientAddressStamper`
     11and `StanzaForwarder`.
     12
     13The former makes sure that all XML stanzas received from the client
     14are stamped with a proper recipient address. The latter
     15passes stanzas on to the stream's avatar.
    1716
    1817TODO:
    1918
    20  * Add docstrings.
    2119 * Add tests.
    2220
     
    2422--- a/wokkel/client.py
    2523+++ b/wokkel/client.py
    26 @@ -21,7 +21,9 @@
    27  from twisted.words.xish import domish
     24@@ -22,7 +22,9 @@
    2825 
    2926 from wokkel import generic
    30 +from wokkel.compat import XmlStreamServerFactory
    3127 from wokkel.iwokkel import IUserSession
    3228+from wokkel.subprotocols import ServerStreamManager
    3329 from wokkel.subprotocols import StreamManager
     30+from wokkel.subprotocols import XMPPHandler
    3431 
    3532 NS_CLIENT = 'jabber:client'
    36 @@ -442,3 +444,41 @@
     33 
     34@@ -480,3 +482,70 @@
    3735             self.portal = self.portals[self.xmlstream.thisEntity]
    3836         except KeyError:
     
    4139+
    4240+
    43 +class XMPPC2SServerFactory(XmlStreamServerFactory):
     41+class RecipientAddressStamper(XMPPHandler):
     42+    """
     43+    Protocol handler to ensure client stanzas have a sender address.
     44+    """
     45+
     46+    def connectionInitialized(self):
     47+        self.xmlstream.addObserver('/*', self.onStanza, priority=1)
     48+
     49+
     50+    def onStanza(self, element):
     51+        """
     52+        Make sure each stanza has a sender address.
     53+        """
     54+        if element.uri:
     55+            return
     56+
     57+        if (element.name == 'presence' and
     58+            element.getAttribute('type') in ('subscribe', 'subscribed',
     59+                                             'unsubscribe', 'unsubscribed')):
     60+            element['from'] = self.xmlstream.avatar.entity.userhost()
     61+        elif element.name in ('message', 'presence', 'iq'):
     62+            element['from'] = self.xmlstream.avatar.entity.full()
     63+
     64+
     65+
     66+class XMPPC2SServerFactory(xmlstream.XmlStreamServerFactory):
     67+    """
     68+    Server factory for XMPP client-server connections.
     69+    """
    4470+
    4571+    def __init__(self, portals):
     
    4773+            return XMPPClientListenAuthenticator(portals)
    4874+
    49 +        XmlStreamServerFactory.__init__(self, authenticatorFactory)
     75+        xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
    5076+        self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
    5177+                          self.onConnectionMade)
     
    74100+        """
    75101+        return [
    76 +            generic.StanzaForwarder()
    77 +        ]
     102+            generic.StanzaForwarder(),
     103+            RecipientAddressStamper(),
     104+            ]
    78105diff --git a/wokkel/generic.py b/wokkel/generic.py
    79106--- a/wokkel/generic.py
    80107+++ b/wokkel/generic.py
    81 @@ -615,3 +615,55 @@
    82  
    83          self._initializers = self.getInitializers()
    84          self._initializeStream()
     108@@ -628,3 +628,65 @@
     109     standard full stop.
     110     """
     111     return name.encode('idna')
    85112+
    86113+
     
    108135+        Called when the stream has been initialized.
    109136+        """
    110 +        self.xmlstream.addObserver(
    111 +            '/*[@xmlns="%s"]' % self.xmlstream.namespace,
    112 +            self.onStanza, priority=-1)
     137+        self.xmlstream.addObserver('/*[@xmlns="%s"]' %
     138+                                       self.xmlstream.namespace,
     139+                                   stripNamespace, priority=1)
     140+        self.xmlstream.addObserver('/*', self.onStanza, priority=-1)
     141+
    113142+
    114143+
     
    117146+        Called when a stanza element was received.
    118147+
    119 +        Unless a stanza has already been handled, or the name of the element is
    120 +        not one of C{'iq'}, C{'message'}, C{'presence'}, the stanza is passed
    121 +        on to the avatar's C{send} method.
     148+        If this is an XML stanza, and it has not been handled by another
     149+        subprotocol handler, the stanza is passed on to the avatar's C{send}
     150+        method.
     151+
     152+        If there is no recipient address on the stanza, a service-unavailable
     153+        is returned instead.
    122154+        """
    123155+        if element.handled:
    124156+            return
    125157+
    126 +        if element.name not in ('iq', 'message', 'presence'):
     158+        if (element.name not in ('iq', 'message', 'presence') or
     159+            element.uri is not None):
    127160+            return
    128161+
    129 +        self.xmlstream.avatar.send(element)
    130 +
    131 +
    132 +    def onError(reason):
     162+        if not element.getAttribute('to'):
     163+            exc = error.StanzaError('service-unavailable')
     164+            self.send(exc.toResponse(element))
     165+        else:
     166+            self.xmlstream.avatar.send(element)
     167+
     168+
     169+    def onError(self, reason):
    133170+        """
    134171+        Log a stream error.
    135172+        """
    136173+        log.err(reason, "Stream error")
    137 diff --git a/wokkel/subprotocols.py b/wokkel/subprotocols.py
    138 --- a/wokkel/subprotocols.py
    139 +++ b/wokkel/subprotocols.py
    140 @@ -142,10 +142,13 @@
    141      @type timeout: C{int}
    142  
    143      @ivar _reactor: A provider of L{IReactorTime} to track timeouts.
    144 +
    145 +    @cvar __streamCount: Global stream count for distinguishing streams.
     174diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
     175--- a/wokkel/test/test_client.py
     176+++ b/wokkel/test/test_client.py
     177@@ -7,7 +7,7 @@
     178 
     179 from base64 import b64encode
     180 
     181-from zope.interface import implements
     182+from zope.interface import implementer
     183 
     184 from twisted.cred.portal import IRealm, Portal
     185 from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
     186@@ -28,6 +28,8 @@
     187 
     188 from wokkel import client, iwokkel
     189 from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator
     190+from wokkel.generic import parseXml
     191+from wokkel.test.helpers import XmlStreamStub
     192 
     193 class XMPPClientTest(unittest.TestCase):
    146194     """
    147  
    148      timeout = None
    149      _reactor = None
    150 +    __streamCount = 0
    151  
    152      logTraffic = False
    153  
    154 @@ -195,20 +198,24 @@
    155          and call each handler's C{makeConnection} method with the L{XmlStream}
    156          instance.
    157          """
    158 -        def logDataIn(buf):
    159 -            log.msg("RECV: %r" % buf)
    160 +        xs.serial = self.__streamCount
    161 +        BaseStreamManager.__streamCount += 1
    162  
    163 -        def logDataOut(buf):
    164 -            log.msg("SEND: %r" % buf)
    165  
    166          if self.logTraffic:
    167 -            xs.rawDataInFn = logDataIn
    168 -            xs.rawDataOutFn = logDataOut
    169 +            def logData(direction, data):
    170 +                log.msg(format="%(direction)s (%(streamID)s): %(data)r",
    171 +                        direction=direction, streamID=xs.serial, data=data)
    172 +
    173 +            log.msg(format="Connection %(streamID)s made", streamID=xs.serial)
    174 +            xs.rawDataInFn = lambda data: logData("RECV", data)
    175 +            xs.rawDataOutFn = lambda data: logData("SEND", data)
    176  
    177          xs.addObserver(xmlstream.STREAM_AUTHD_EVENT,
    178                         self.connectionInitialized)
    179          xs.addObserver(xmlstream.STREAM_END_EVENT,
    180                         self.connectionLost)
    181 +
    182          self.xmlstream = xs
    183  
    184          for e in list(self):
    185 @@ -222,6 +229,9 @@
    186          Send out cached stanzas and call each handler's
    187          C{connectionInitialized} method.
    188          """
    189 +        if self.logTraffic:
    190 +            log.msg(format="Connection %(streamID)s initialized",
    191 +                    streamID=xs.serial)
    192  
    193          xs.addObserver('/iq[@type="result"]', self._onIQResponse)
    194          xs.addObserver('/iq[@type="error"]', self._onIQResponse)
    195 @@ -247,6 +257,10 @@
    196          L{XmlStream} anymore and notifies each handler that the connection
    197          was lost by calling its C{connectionLost} method.
    198          """
    199 +        if self.logTraffic:
    200 +            log.msg(format="Connection %(streamID)s lost",
    201 +                    streamID=self.xmlstream.serial)
    202 +
    203          self.xmlstream = None
    204          self._initialized = False
    205  
     195@@ -180,8 +182,8 @@
     196 
     197 
     198 
     199+@implementer(iwokkel.IUserSession)
     200 class TestSession(object):
     201-    implements(iwokkel.IUserSession)
     202 
     203     def __init__(self, domain, user):
     204         self.domain = domain
     205@@ -189,14 +191,14 @@
     206 
     207 
     208     def bindResource(self, resource):
     209-        return defer.succeed(JID(tuple=(self.user, self.domain, resource)))
     210+        self.entity = JID(tuple=(self.user, self.domain, resource))
     211+        return defer.succeed(self.entity)
     212 
     213 
     214 
     215+@implementer(IRealm)
     216 class TestRealm(object):
     217 
     218-    implements(IRealm)
     219-
     220     logoutCalled = False
     221 
     222     def __init__(self, domain):
     223@@ -679,3 +681,91 @@
     224                          "to='example.com' "
     225                          "version='1.0'>")
     226         self.xmlstream.assertStreamError(self, condition='host-unknown')
     227+
     228+
     229+
     230+class RecipientAddressStamperTest(unittest.TestCase):
     231+    """
     232+    Tests for L{client.RecipientAddressStamper}.
     233+    """
     234+
     235+
     236+    def setUp(self):
     237+        self.stub = XmlStreamStub()
     238+        self.stub.xmlstream.namespace = ''
     239+        avatar = TestSession(u'example.org', u'test')
     240+        avatar.bindResource(u'Home')
     241+        self.stub.xmlstream.avatar = avatar
     242+
     243+        self.protocol = client.RecipientAddressStamper()
     244+        self.protocol.makeConnection(self.stub.xmlstream)
     245+        self.protocol.connectionInitialized()
     246+
     247+
     248+    def test_presence(self):
     249+        """
     250+        The from address is set to the full JID on presence stanzas.
     251+        """
     252+        xml = """<presence/>"""
     253+        element = parseXml(xml)
     254+        self.stub.xmlstream.dispatch(element)
     255+        self.assertEqual(u'test@example.org/Home',
     256+                         element.getAttribute('from'))
     257+
     258+
     259+    def test_presenceSubscribe(self):
     260+        """
     261+        The from address is set to the bare JID on presence subscribe.
     262+        """
     263+        xml = """<presence type='subscribe'/>"""
     264+        element = parseXml(xml)
     265+        self.stub.xmlstream.dispatch(element)
     266+        self.assertEqual(u'test@example.org',
     267+                         element.getAttribute('from'))
     268+
     269+
     270+    def test_fromAlreadySet(self):
     271+        """
     272+        The from address is overridden if already present.
     273+        """
     274+        xml = """<presence from='test@example.org/Work'/>"""
     275+        element = parseXml(xml)
     276+        self.stub.xmlstream.dispatch(element)
     277+        self.assertEqual(u'test@example.org/Home',
     278+                         element.getAttribute('from'))
     279+
     280+
     281+    def test_notHandled(self):
     282+        """
     283+        The stanza will not have its 'handled' attribute set to True.
     284+        """
     285+        xml = """<presence/>"""
     286+        element = parseXml(xml)
     287+        self.stub.xmlstream.dispatch(element)
     288+        self.assertFalse(element.handled)
     289+
     290+
     291+    def test_message(self):
     292+        """
     293+        The from address is set to the full JID on message stanzas.
     294+        """
     295+        xml = """<message to='other@example.org'>
     296+                   <body>Hi!</body>
     297+                 </message>"""
     298+        element = parseXml(xml)
     299+        self.stub.xmlstream.dispatch(element)
     300+        self.assertEqual(u'test@example.org/Home',
     301+                         element.getAttribute('from'))
     302+
     303+
     304+    def test_iq(self):
     305+        """
     306+        The from address is set to the full JID on iq stanzas.
     307+        """
     308+        xml = """<iq type='get' id='g_1'>
     309+                   <query xmlns='jabber:iq:version'/>
     310+                 </iq>"""
     311+        element = parseXml(xml)
     312+        self.stub.xmlstream.dispatch(element)
     313+        self.assertEqual(u'test@example.org/Home',
     314+                         element.getAttribute('from'))
     315diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
     316--- a/wokkel/test/test_generic.py
     317+++ b/wokkel/test/test_generic.py
     318@@ -681,3 +681,105 @@
     319         name = u"example.com."
     320         result = generic.prepareIDNName(name)
     321         self.assertEqual(b"example.com.", result)
     322+
     323+
     324+
     325+class StanzaForwarderTest(unittest.TestCase):
     326+    """
     327+    Tests for L{generic.StanzaForwarder}.
     328+    """
     329+
     330+    def setUp(self):
     331+        class Avatar(object):
     332+            def __init__(self):
     333+                self.sent = []
     334+
     335+            def send(self, element):
     336+                self.sent.append(element)
     337+
     338+        self.stub = XmlStreamStub()
     339+        self.avatar = Avatar()
     340+        self.protocol = generic.StanzaForwarder()
     341+        self.protocol.makeConnection(self.stub.xmlstream)
     342+        self.protocol.xmlstream.avatar = self.avatar
     343+        self.protocol.xmlstream.namespace = u'jabber:client'
     344+        self.protocol.send = self.protocol.xmlstream.send
     345+
     346+
     347+    def test_onStanza(self):
     348+        """
     349+        An XML stanza is delivered at the stream avatar.
     350+        """
     351+        self.protocol.connectionInitialized()
     352+
     353+        element = domish.Element((None, u'message'))
     354+        element[u'to'] = u'other@example.org'
     355+        self.stub.send(element)
     356+
     357+        self.assertEqual(1, len(self.avatar.sent))
     358+        self.assertEqual(0, len(self.stub.output))
     359+
     360+
     361+    def test_onStanzaNoRecipient(self):
     362+        """
     363+        Stanzas without recipient are rejected.
     364+        """
     365+        self.protocol.connectionInitialized()
     366+
     367+        element = domish.Element((None, u'message'))
     368+        self.stub.send(element)
     369+
     370+        self.assertEqual(0, len(self.avatar.sent))
     371+        self.assertEqual(1, len(self.stub.output))
     372+
     373+
     374+    def test_onStanzaClientNamespace(self):
     375+        """
     376+        Stanzas with an explicit namespace are delivered.
     377+        """
     378+        self.protocol.connectionInitialized()
     379+
     380+        element = domish.Element(('jabber:client', u'message'))
     381+        element[u'to'] = u'other@example.org'
     382+        self.stub.send(element)
     383+
     384+        self.assertEqual(1, len(self.avatar.sent))
     385+        self.assertEqual(0, len(self.stub.output))
     386+
     387+
     388+    def test_onStanzaWrongNamespace(self):
     389+        """
     390+        If there is no xmlns on the stanza, it should still be delivered.
     391+        """
     392+        self.protocol.connectionInitialized()
     393+
     394+        element = domish.Element((u'testns', u'message'))
     395+        element[u'to'] = u'other@example.org'
     396+        self.stub.send(element)
     397+
     398+        self.assertEqual(0, len(self.avatar.sent))
     399+        self.assertEqual(0, len(self.stub.output))
     400+
     401+
     402+    def test_onStanzaAlreadyHandled(self):
     403+        """
     404+        If the stanza is marked as handled, ignore it.
     405+        """
     406+        self.protocol.connectionInitialized()
     407+
     408+        element = domish.Element((None, u'message'))
     409+        element[u'to'] = u'other@example.org'
     410+        element.handled = True
     411+        self.stub.send(element)
     412+
     413+        self.assertEqual(0, len(self.avatar.sent))
     414+        self.assertEqual(0, len(self.stub.output))
     415+
     416+
     417+    def test_onError(self):
     418+        """
     419+        A stream error is logged.
     420+        """
     421+        exc = error.StreamError('host-unknown')
     422+        self.stub.xmlstream.dispatch(exc, xmlstream.STREAM_ERROR_EVENT)
     423+        self.assertEqual(1, len(self.flushLoggedErrors(error.StreamError)))
Note: See TracChangeset for help on using the changeset viewer.