source: ralphm-patches/c2s_server_factory.patch @ 72:727b4d29c48e

Last change on this file since 72:727b4d29c48e was 72:727b4d29c48e, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Major reworking of avatars, session manager and stanza handlers.

File size: 12.5 KB
  • wokkel/client.py

    # HG changeset patch
    # Parent 49294b2cf829414b42141731b5130d91474c0443
    Add factory for accepting client connections.
    
    The new `XMPPC2SServerFactory` is a server factory for accepting client
    connections. It uses `XMPPClientListenAuthenticator` to perform the
    steps for authentication and binding of a resource.
    
    For each connection, the factory also sets up subprotocol handlers by
    calling `setupHandlers`. By default these are `RecipientAddressStamper`
    and `StanzaForwarder`.
    
    The former makes sure that all XML stanzas received from the client
    are stamped with a proper recipient address. The latter
    passes stanzas on to the stream's avatar.
    
    TODO:
    
     * Add tests.
    
    diff --git a/wokkel/client.py b/wokkel/client.py
    a b  
    2222
    2323from wokkel import generic
    2424from wokkel.iwokkel import IUserSession
     25from wokkel.subprotocols import ServerStreamManager
    2526from wokkel.subprotocols import StreamManager
     27from wokkel.subprotocols import XMPPHandler
    2628
    2729NS_CLIENT = 'jabber:client'
    2830
     
    480482            self.portal = self.portals[self.xmlstream.thisEntity]
    481483        except KeyError:
    482484            raise error.StreamError('host-unknown')
     485
     486
     487
     488class RecipientAddressStamper(XMPPHandler):
     489    """
     490    Protocol handler to ensure client stanzas have a sender address.
     491    """
     492
     493    def connectionInitialized(self):
     494        self.xmlstream.addObserver('/*', self.onStanza, priority=1)
     495
     496
     497    def onStanza(self, element):
     498        """
     499        Make sure each stanza has a sender address.
     500        """
     501        if element.uri:
     502            return
     503
     504        if (element.name == 'presence' and
     505            element.getAttribute('type') in ('subscribe', 'subscribed',
     506                                             'unsubscribe', 'unsubscribed')):
     507            element['from'] = self.xmlstream.avatar.entity.userhost()
     508        elif element.name in ('message', 'presence', 'iq'):
     509            element['from'] = self.xmlstream.avatar.entity.full()
     510
     511
     512
     513class XMPPC2SServerFactory(xmlstream.XmlStreamServerFactory):
     514    """
     515    Server factory for XMPP client-server connections.
     516    """
     517
     518    def __init__(self, portals):
     519        def authenticatorFactory():
     520            return XMPPClientListenAuthenticator(portals)
     521
     522        xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
     523        self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
     524                          self.onConnectionMade)
     525
     526
     527    def onConnectionMade(self, xs):
     528        """
     529        Called when a connection is made.
     530
     531        This creates a stream manager, calls L{setupHandlers} to attach
     532        subprotocol handlers and then signals the stream manager that
     533        the connection was made.
     534        """
     535        sm = ServerStreamManager()
     536        sm.logTraffic = self.logTraffic
     537
     538        for handler in self.setupHandlers():
     539            handler.setHandlerParent(sm)
     540
     541        sm.makeConnection(xs)
     542
     543
     544    def setupHandlers(self):
     545        """
     546        Set up XMPP subprotocol handlers.
     547        """
     548        return [
     549            generic.StanzaForwarder(),
     550            RecipientAddressStamper(),
     551            ]
  • wokkel/generic.py

    diff --git a/wokkel/generic.py b/wokkel/generic.py
    a b  
    628628    standard full stop.
    629629    """
    630630    return name.encode('idna')
     631
     632
     633
     634class StanzaForwarder(XMPPHandler):
     635    """
     636    XMPP protocol for passing incoming stanzas to the stream avatar.
     637
     638    This handler adds an observer for all XML Stanzas to forward to the C{send}
     639    method on the cred avatar set on the XML Stream, unless it has been handled
     640    by other observers.
     641
     642    Stream errors are logged.
     643    """
     644
     645    def connectionMade(self):
     646        """
     647        Called when a connection is made.
     648        """
     649        self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
     650
     651
     652    def connectionInitialized(self):
     653        """
     654        Called when the stream has been initialized.
     655        """
     656        self.xmlstream.addObserver('/*[@xmlns="%s"]' %
     657                                       self.xmlstream.namespace,
     658                                   stripNamespace, priority=1)
     659        self.xmlstream.addObserver('/*', self.onStanza, priority=-1)
     660
     661
     662
     663    def onStanza(self, element):
     664        """
     665        Called when a stanza element was received.
     666
     667        If this is an XML stanza, and it has not been handled by another
     668        subprotocol handler, the stanza is passed on to the avatar's C{send}
     669        method.
     670
     671        If there is no recipient address on the stanza, a service-unavailable
     672        is returned instead.
     673        """
     674        if element.handled:
     675            return
     676
     677        if (element.name not in ('iq', 'message', 'presence') or
     678            element.uri is not None):
     679            return
     680
     681        if not element.getAttribute('to'):
     682            exc = error.StanzaError('service-unavailable')
     683            self.send(exc.toResponse(element))
     684        else:
     685            self.xmlstream.avatar.send(element)
     686
     687
     688    def onError(self, reason):
     689        """
     690        Log a stream error.
     691        """
     692        log.err(reason, "Stream error")
  • wokkel/test/test_client.py

    diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
    a b  
    77
    88from base64 import b64encode
    99
    10 from zope.interface import implements
     10from zope.interface import implementer
    1111
    1212from twisted.cred.portal import IRealm, Portal
    1313from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
     
    2828
    2929from wokkel import client, iwokkel
    3030from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator
     31from wokkel.generic import parseXml
     32from wokkel.test.helpers import XmlStreamStub
    3133
    3234class XMPPClientTest(unittest.TestCase):
    3335    """
     
    180182
    181183
    182184
     185@implementer(iwokkel.IUserSession)
    183186class TestSession(object):
    184     implements(iwokkel.IUserSession)
    185187
    186188    def __init__(self, domain, user):
    187189        self.domain = domain
     
    189191
    190192
    191193    def bindResource(self, resource):
    192         return defer.succeed(JID(tuple=(self.user, self.domain, resource)))
     194        self.entity = JID(tuple=(self.user, self.domain, resource))
     195        return defer.succeed(self.entity)
    193196
    194197
    195198
     199@implementer(IRealm)
    196200class TestRealm(object):
    197201
    198     implements(IRealm)
    199 
    200202    logoutCalled = False
    201203
    202204    def __init__(self, domain):
     
    679681                         "to='example.com' "
    680682                         "version='1.0'>")
    681683        self.xmlstream.assertStreamError(self, condition='host-unknown')
     684
     685
     686
     687class RecipientAddressStamperTest(unittest.TestCase):
     688    """
     689    Tests for L{client.RecipientAddressStamper}.
     690    """
     691
     692
     693    def setUp(self):
     694        self.stub = XmlStreamStub()
     695        self.stub.xmlstream.namespace = ''
     696        avatar = TestSession(u'example.org', u'test')
     697        avatar.bindResource(u'Home')
     698        self.stub.xmlstream.avatar = avatar
     699
     700        self.protocol = client.RecipientAddressStamper()
     701        self.protocol.makeConnection(self.stub.xmlstream)
     702        self.protocol.connectionInitialized()
     703
     704
     705    def test_presence(self):
     706        """
     707        The from address is set to the full JID on presence stanzas.
     708        """
     709        xml = """<presence/>"""
     710        element = parseXml(xml)
     711        self.stub.xmlstream.dispatch(element)
     712        self.assertEqual(u'test@example.org/Home',
     713                         element.getAttribute('from'))
     714
     715
     716    def test_presenceSubscribe(self):
     717        """
     718        The from address is set to the bare JID on presence subscribe.
     719        """
     720        xml = """<presence type='subscribe'/>"""
     721        element = parseXml(xml)
     722        self.stub.xmlstream.dispatch(element)
     723        self.assertEqual(u'test@example.org',
     724                         element.getAttribute('from'))
     725
     726
     727    def test_fromAlreadySet(self):
     728        """
     729        The from address is overridden if already present.
     730        """
     731        xml = """<presence from='test@example.org/Work'/>"""
     732        element = parseXml(xml)
     733        self.stub.xmlstream.dispatch(element)
     734        self.assertEqual(u'test@example.org/Home',
     735                         element.getAttribute('from'))
     736
     737
     738    def test_notHandled(self):
     739        """
     740        The stanza will not have its 'handled' attribute set to True.
     741        """
     742        xml = """<presence/>"""
     743        element = parseXml(xml)
     744        self.stub.xmlstream.dispatch(element)
     745        self.assertFalse(element.handled)
     746
     747
     748    def test_message(self):
     749        """
     750        The from address is set to the full JID on message stanzas.
     751        """
     752        xml = """<message to='other@example.org'>
     753                   <body>Hi!</body>
     754                 </message>"""
     755        element = parseXml(xml)
     756        self.stub.xmlstream.dispatch(element)
     757        self.assertEqual(u'test@example.org/Home',
     758                         element.getAttribute('from'))
     759
     760
     761    def test_iq(self):
     762        """
     763        The from address is set to the full JID on iq stanzas.
     764        """
     765        xml = """<iq type='get' id='g_1'>
     766                   <query xmlns='jabber:iq:version'/>
     767                 </iq>"""
     768        element = parseXml(xml)
     769        self.stub.xmlstream.dispatch(element)
     770        self.assertEqual(u'test@example.org/Home',
     771                         element.getAttribute('from'))
  • wokkel/test/test_generic.py

    diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    a b  
    681681        name = u"example.com."
    682682        result = generic.prepareIDNName(name)
    683683        self.assertEqual(b"example.com.", result)
     684
     685
     686
     687class StanzaForwarderTest(unittest.TestCase):
     688    """
     689    Tests for L{generic.StanzaForwarder}.
     690    """
     691
     692    def setUp(self):
     693        class Avatar(object):
     694            def __init__(self):
     695                self.sent = []
     696
     697            def send(self, element):
     698                self.sent.append(element)
     699
     700        self.stub = XmlStreamStub()
     701        self.avatar = Avatar()
     702        self.protocol = generic.StanzaForwarder()
     703        self.protocol.makeConnection(self.stub.xmlstream)
     704        self.protocol.xmlstream.avatar = self.avatar
     705        self.protocol.xmlstream.namespace = u'jabber:client'
     706        self.protocol.send = self.protocol.xmlstream.send
     707
     708
     709    def test_onStanza(self):
     710        """
     711        An XML stanza is delivered at the stream avatar.
     712        """
     713        self.protocol.connectionInitialized()
     714
     715        element = domish.Element((None, u'message'))
     716        element[u'to'] = u'other@example.org'
     717        self.stub.send(element)
     718
     719        self.assertEqual(1, len(self.avatar.sent))
     720        self.assertEqual(0, len(self.stub.output))
     721
     722
     723    def test_onStanzaNoRecipient(self):
     724        """
     725        Stanzas without recipient are rejected.
     726        """
     727        self.protocol.connectionInitialized()
     728
     729        element = domish.Element((None, u'message'))
     730        self.stub.send(element)
     731
     732        self.assertEqual(0, len(self.avatar.sent))
     733        self.assertEqual(1, len(self.stub.output))
     734
     735
     736    def test_onStanzaClientNamespace(self):
     737        """
     738        Stanzas with an explicit namespace are delivered.
     739        """
     740        self.protocol.connectionInitialized()
     741
     742        element = domish.Element(('jabber:client', u'message'))
     743        element[u'to'] = u'other@example.org'
     744        self.stub.send(element)
     745
     746        self.assertEqual(1, len(self.avatar.sent))
     747        self.assertEqual(0, len(self.stub.output))
     748
     749
     750    def test_onStanzaWrongNamespace(self):
     751        """
     752        If there is no xmlns on the stanza, it should still be delivered.
     753        """
     754        self.protocol.connectionInitialized()
     755
     756        element = domish.Element((u'testns', u'message'))
     757        element[u'to'] = u'other@example.org'
     758        self.stub.send(element)
     759
     760        self.assertEqual(0, len(self.avatar.sent))
     761        self.assertEqual(0, len(self.stub.output))
     762
     763
     764    def test_onStanzaAlreadyHandled(self):
     765        """
     766        If the stanza is marked as handled, ignore it.
     767        """
     768        self.protocol.connectionInitialized()
     769
     770        element = domish.Element((None, u'message'))
     771        element[u'to'] = u'other@example.org'
     772        element.handled = True
     773        self.stub.send(element)
     774
     775        self.assertEqual(0, len(self.avatar.sent))
     776        self.assertEqual(0, len(self.stub.output))
     777
     778
     779    def test_onError(self):
     780        """
     781        A stream error is logged.
     782        """
     783        exc = error.StreamError('host-unknown')
     784        self.stub.xmlstream.dispatch(exc, xmlstream.STREAM_ERROR_EVENT)
     785        self.assertEqual(1, len(self.flushLoggedErrors(error.StreamError)))
Note: See TracBrowser for help on using the repository browser.