source: ralphm-patches/c2s_server_factory.patch @ 83:255aae0cf8c5

Last change on this file since 83:255aae0cf8c5 was 80:80ed2848c4e0, checked in by Ralph Meijer <ralphm@…>, 4 years ago

Don't depend on the incoming element being stripped.

File size: 12.0 KB
  • wokkel/client.py

    # HG changeset patch
    # Parent 49294b2cf829414b42141731b5130d91474c0443
    # Parent  0a68c01ed5b5f429eed343df521bc706fe88afd1
    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 != self.xmlstream.namespace:
     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  
    480480    standard full stop.
    481481    """
    482482    return name.encode('idna')
     483
     484
     485
     486class StanzaForwarder(XMPPHandler):
     487    """
     488    XMPP protocol for passing incoming stanzas to the stream avatar.
     489
     490    This handler adds an observer for all XML Stanzas to forward to the C{send}
     491    method on the cred avatar set on the XML Stream, unless it has been handled
     492    by other observers.
     493
     494    Stream errors are logged.
     495    """
     496
     497    def connectionMade(self):
     498        """
     499        Called when a connection is made.
     500        """
     501        self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
     502
     503
     504    def connectionInitialized(self):
     505        """
     506        Called when the stream has been initialized.
     507        """
     508        self.xmlstream.addObserver('/*', self.onStanza, priority=-1)
     509
     510
     511
     512    def onStanza(self, element):
     513        """
     514        Called when a stanza element was received.
     515
     516        If this is an XML stanza, and it has not been handled by another
     517        subprotocol handler, the stanza is passed on to the avatar's C{send}
     518        method.
     519
     520        If there is no recipient address on the stanza, a service-unavailable
     521        is returned instead.
     522        """
     523        if element.handled:
     524            return
     525
     526        if (element.name not in ('iq', 'message', 'presence') or
     527            element.uri != self.xmlstream.namespace):
     528            return
     529
     530        stanza = Stanza.fromElement(element)
     531
     532        if not stanza.recipient:
     533            exc = error.StanzaError('service-unavailable')
     534            self.send(exc.toResponse(stanza.element))
     535        else:
     536            self.xmlstream.avatar.send(stanza.element)
     537
     538
     539    def onError(self, reason):
     540        """
     541        Log a stream error.
     542        """
     543        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  
    728728        name = u"example.com."
    729729        result = generic.prepareIDNName(name)
    730730        self.assertEqual(b"example.com.", result)
     731
     732
     733
     734class StanzaForwarderTest(unittest.TestCase):
     735    """
     736    Tests for L{generic.StanzaForwarder}.
     737    """
     738
     739    def setUp(self):
     740        class Avatar(object):
     741            def __init__(self):
     742                self.sent = []
     743
     744            def send(self, element):
     745                self.sent.append(element)
     746
     747        self.stub = XmlStreamStub()
     748        self.avatar = Avatar()
     749        self.protocol = generic.StanzaForwarder()
     750        self.protocol.makeConnection(self.stub.xmlstream)
     751        self.protocol.xmlstream.avatar = self.avatar
     752        self.protocol.xmlstream.namespace = u'jabber:client'
     753        self.protocol.send = self.protocol.xmlstream.send
     754
     755
     756    def test_onStanza(self):
     757        """
     758        An XML stanza is delivered at the stream avatar.
     759        """
     760        self.protocol.connectionInitialized()
     761
     762        element = domish.Element((u'jabber:client', u'message'))
     763        element[u'to'] = u'other@example.org'
     764        self.stub.send(element)
     765
     766        self.assertEqual(1, len(self.avatar.sent))
     767        self.assertEqual(0, len(self.stub.output))
     768
     769
     770    def test_onStanzaNoRecipient(self):
     771        """
     772        Stanzas without recipient are rejected.
     773        """
     774        self.protocol.connectionInitialized()
     775
     776        element = domish.Element((u'jabber:client', u'message'))
     777        self.stub.send(element)
     778
     779        self.assertEqual(0, len(self.avatar.sent))
     780        self.assertEqual(1, len(self.stub.output))
     781
     782
     783    def test_onStanzaWrongNamespace(self):
     784        """
     785        If there is no xmlns on the stanza, it should still be delivered.
     786        """
     787        self.protocol.connectionInitialized()
     788
     789        element = domish.Element((u'testns', u'message'))
     790        element[u'to'] = u'other@example.org'
     791        self.stub.send(element)
     792
     793        self.assertEqual(0, len(self.avatar.sent))
     794        self.assertEqual(0, len(self.stub.output))
     795
     796
     797    def test_onStanzaAlreadyHandled(self):
     798        """
     799        If the stanza is marked as handled, ignore it.
     800        """
     801        self.protocol.connectionInitialized()
     802
     803        element = domish.Element((None, u'message'))
     804        element[u'to'] = u'other@example.org'
     805        element.handled = True
     806        self.stub.send(element)
     807
     808        self.assertEqual(0, len(self.avatar.sent))
     809        self.assertEqual(0, len(self.stub.output))
     810
     811
     812    def test_onError(self):
     813        """
     814        A stream error is logged.
     815        """
     816        exc = error.StreamError('host-unknown')
     817        self.stub.xmlstream.dispatch(exc, xmlstream.STREAM_ERROR_EVENT)
     818        self.assertEqual(1, len(self.flushLoggedErrors(error.StreamError)))
Note: See TracBrowser for help on using the repository browser.