source: ralphm-patches/c2s_server_factory.patch

Last change on this file was 80:80ed2848c4e0, checked in by Ralph Meijer <ralphm@…>, 5 years ago

Don't depend on the incoming element being stripped.

File size: 12.0 KB
RevLine 
[54]1# HG changeset patch
[72]2# Parent 49294b2cf829414b42141731b5130d91474c0443
[80]3# Parent  0a68c01ed5b5f429eed343df521bc706fe88afd1
[54]4Add factory for accepting client connections.
5
[72]6The new `XMPPC2SServerFactory` is a server factory for accepting client
[54]7connections. It uses `XMPPClientListenAuthenticator` to perform the
[72]8steps for authentication and binding of a resource.
[54]9
[72]10For each connection, the factory also sets up subprotocol handlers by
11calling `setupHandlers`. By default these are `RecipientAddressStamper`
12and `StanzaForwarder`.
[54]13
[72]14The former makes sure that all XML stanzas received from the client
15are stamped with a proper recipient address. The latter
16passes stanzas on to the stream's avatar.
[54]17
18TODO:
19
20 * Add tests.
21
[66]22diff --git a/wokkel/client.py b/wokkel/client.py
23--- a/wokkel/client.py
24+++ b/wokkel/client.py
[72]25@@ -22,7 +22,9 @@
[54]26 
27 from wokkel import generic
[66]28 from wokkel.iwokkel import IUserSession
[68]29+from wokkel.subprotocols import ServerStreamManager
[54]30 from wokkel.subprotocols import StreamManager
[72]31+from wokkel.subprotocols import XMPPHandler
[54]32 
[68]33 NS_CLIENT = 'jabber:client'
[72]34 
35@@ -480,3 +482,70 @@
[66]36             self.portal = self.portals[self.xmlstream.thisEntity]
37         except KeyError:
38             raise error.StreamError('host-unknown')
[54]39+
40+
41+
[72]42+class RecipientAddressStamper(XMPPHandler):
43+    """
44+    Protocol handler to ensure client stanzas have a sender address.
45+    """
46+
47+    def connectionInitialized(self):
48+        self.xmlstream.addObserver('/*', self.onStanza, priority=1)
49+
50+
51+    def onStanza(self, element):
52+        """
53+        Make sure each stanza has a sender address.
54+        """
[80]55+        if element.uri != self.xmlstream.namespace:
[72]56+            return
57+
58+        if (element.name == 'presence' and
59+            element.getAttribute('type') in ('subscribe', 'subscribed',
60+                                             'unsubscribe', 'unsubscribed')):
61+            element['from'] = self.xmlstream.avatar.entity.userhost()
62+        elif element.name in ('message', 'presence', 'iq'):
63+            element['from'] = self.xmlstream.avatar.entity.full()
64+
65+
66+
67+class XMPPC2SServerFactory(xmlstream.XmlStreamServerFactory):
68+    """
69+    Server factory for XMPP client-server connections.
70+    """
[54]71+
[68]72+    def __init__(self, portals):
[54]73+        def authenticatorFactory():
[68]74+            return XMPPClientListenAuthenticator(portals)
[54]75+
[72]76+        xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
[54]77+        self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
78+                          self.onConnectionMade)
79+
80+
81+    def onConnectionMade(self, xs):
82+        """
[68]83+        Called when a connection is made.
[54]84+
[68]85+        This creates a stream manager, calls L{setupHandlers} to attach
86+        subprotocol handlers and then signals the stream manager that
87+        the connection was made.
[54]88+        """
[68]89+        sm = ServerStreamManager()
90+        sm.logTraffic = self.logTraffic
[54]91+
[68]92+        for handler in self.setupHandlers():
93+            handler.setHandlerParent(sm)
[54]94+
[68]95+        sm.makeConnection(xs)
[54]96+
97+
[68]98+    def setupHandlers(self):
99+        """
100+        Set up XMPP subprotocol handlers.
101+        """
102+        return [
[72]103+            generic.StanzaForwarder(),
104+            RecipientAddressStamper(),
105+            ]
[68]106diff --git a/wokkel/generic.py b/wokkel/generic.py
107--- a/wokkel/generic.py
108+++ b/wokkel/generic.py
[79]109@@ -480,3 +480,64 @@
[72]110     standard full stop.
111     """
112     return name.encode('idna')
[68]113+
114+
115+
116+class StanzaForwarder(XMPPHandler):
117+    """
118+    XMPP protocol for passing incoming stanzas to the stream avatar.
119+
120+    This handler adds an observer for all XML Stanzas to forward to the C{send}
121+    method on the cred avatar set on the XML Stream, unless it has been handled
122+    by other observers.
123+
124+    Stream errors are logged.
125+    """
126+
127+    def connectionMade(self):
128+        """
129+        Called when a connection is made.
130+        """
131+        self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
132+
133+
134+    def connectionInitialized(self):
135+        """
136+        Called when the stream has been initialized.
137+        """
[72]138+        self.xmlstream.addObserver('/*', self.onStanza, priority=-1)
139+
[68]140+
141+
142+    def onStanza(self, element):
143+        """
144+        Called when a stanza element was received.
145+
[72]146+        If this is an XML stanza, and it has not been handled by another
147+        subprotocol handler, the stanza is passed on to the avatar's C{send}
148+        method.
149+
150+        If there is no recipient address on the stanza, a service-unavailable
151+        is returned instead.
[68]152+        """
153+        if element.handled:
154+            return
155+
[72]156+        if (element.name not in ('iq', 'message', 'presence') or
[79]157+            element.uri != self.xmlstream.namespace):
[68]158+            return
159+
[79]160+        stanza = Stanza.fromElement(element)
161+
162+        if not stanza.recipient:
[72]163+            exc = error.StanzaError('service-unavailable')
[79]164+            self.send(exc.toResponse(stanza.element))
[72]165+        else:
[79]166+            self.xmlstream.avatar.send(stanza.element)
[68]167+
168+
[72]169+    def onError(self, reason):
[68]170+        """
171+        Log a stream error.
172+        """
173+        log.err(reason, "Stream error")
[72]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 @@
[68]178 
[72]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):
194     """
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')
[68]227+
228+
229+
[72]230+class RecipientAddressStamperTest(unittest.TestCase):
231+    """
232+    Tests for L{client.RecipientAddressStamper}.
233+    """
[54]234+
[72]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
[79]318@@ -728,3 +728,91 @@
[72]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+
[79]353+        element = domish.Element((u'jabber:client', u'message'))
[72]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+
[79]367+        element = domish.Element((u'jabber:client', u'message'))
[72]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_onStanzaWrongNamespace(self):
375+        """
376+        If there is no xmlns on the stanza, it should still be delivered.
377+        """
378+        self.protocol.connectionInitialized()
379+
380+        element = domish.Element((u'testns', u'message'))
381+        element[u'to'] = u'other@example.org'
382+        self.stub.send(element)
383+
384+        self.assertEqual(0, len(self.avatar.sent))
385+        self.assertEqual(0, len(self.stub.output))
386+
387+
388+    def test_onStanzaAlreadyHandled(self):
389+        """
390+        If the stanza is marked as handled, ignore it.
391+        """
392+        self.protocol.connectionInitialized()
393+
394+        element = domish.Element((None, u'message'))
395+        element[u'to'] = u'other@example.org'
396+        element.handled = True
397+        self.stub.send(element)
398+
399+        self.assertEqual(0, len(self.avatar.sent))
400+        self.assertEqual(0, len(self.stub.output))
401+
402+
403+    def test_onError(self):
404+        """
405+        A stream error is logged.
406+        """
407+        exc = error.StreamError('host-unknown')
408+        self.stub.xmlstream.dispatch(exc, xmlstream.STREAM_ERROR_EVENT)
409+        self.assertEqual(1, len(self.flushLoggedErrors(error.StreamError)))
Note: See TracBrowser for help on using the repository browser.