source: wokkel/generic.py @ 171:4f5ee7b81ebf

Last change on this file since 171:4f5ee7b81ebf was 171:4f5ee7b81ebf, checked in by Ralph Meijer <ralphm@…>, 10 years ago

Add convenience method Request.parseRequest.

The new method Request.parseRequest is called from Request.fromElement
with the child element of the iq that is passed to fromElement, providing
a convenient hook for parsing the payload of incoming requests.

  • Property exe set to *
File size: 9.5 KB
Line 
1# -*- test-case-name: wokkel.test.test_generic -*-
2#
3# Copyright (c) Ralph Meijer.
4# See LICENSE for details.
5
6"""
7Generic XMPP protocol helpers.
8"""
9
10from zope.interface import implements
11
12from twisted.internet import defer, protocol
13from twisted.python import reflect
14from twisted.words.protocols.jabber import error, jid, xmlstream
15from twisted.words.protocols.jabber.xmlstream import toResponse
16from twisted.words.xish import domish, utility
17from twisted.words.xish.xmlstream import BootstrapMixin
18
19from wokkel.iwokkel import IDisco
20from wokkel.subprotocols import XMPPHandler
21
22IQ_GET = '/iq[@type="get"]'
23IQ_SET = '/iq[@type="set"]'
24
25NS_VERSION = 'jabber:iq:version'
26VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
27
28def parseXml(string):
29    """
30    Parse serialized XML into a DOM structure.
31
32    @param string: The serialized XML to be parsed, UTF-8 encoded.
33    @type string: C{str}.
34    @return: The DOM structure, or C{None} on empty or incomplete input.
35    @rtype: L{domish.Element}
36    """
37    roots = []
38    results = []
39    elementStream = domish.elementStream()
40    elementStream.DocumentStartEvent = roots.append
41    elementStream.ElementEvent = lambda elem: roots[0].addChild(elem)
42    elementStream.DocumentEndEvent = lambda: results.append(roots[0])
43    elementStream.parse(string)
44    return results and results[0] or None
45
46
47
48def stripNamespace(rootElement):
49    namespace = rootElement.uri
50
51    def strip(element):
52        if element.uri == namespace:
53            element.uri = None
54            if element.defaultUri == namespace:
55                element.defaultUri = None
56            for child in element.elements():
57                strip(child)
58
59    if namespace is not None:
60        strip(rootElement)
61
62    return rootElement
63
64
65
66class FallbackHandler(XMPPHandler):
67    """
68    XMPP subprotocol handler that catches unhandled iq requests.
69
70    Unhandled iq requests are replied to with a service-unavailable stanza
71    error.
72    """
73
74    def connectionInitialized(self):
75        self.xmlstream.addObserver(IQ_SET, self.iqFallback, -1)
76        self.xmlstream.addObserver(IQ_GET, self.iqFallback, -1)
77
78    def iqFallback(self, iq):
79        if iq.handled == True:
80            return
81
82        reply = error.StanzaError('service-unavailable')
83        self.xmlstream.send(reply.toResponse(iq))
84
85
86
87class VersionHandler(XMPPHandler):
88    """
89    XMPP subprotocol handler for XMPP Software Version.
90
91    This protocol is described in
92    U{XEP-0092<http://xmpp.org/extensions/xep-0092.html>}.
93    """
94
95    implements(IDisco)
96
97    def __init__(self, name, version):
98        self.name = name
99        self.version = version
100
101    def connectionInitialized(self):
102        self.xmlstream.addObserver(VERSION, self.onVersion)
103
104    def onVersion(self, iq):
105        response = toResponse(iq, "result")
106
107        query = response.addElement((NS_VERSION, "query"))
108        query.addElement("name", content=self.name)
109        query.addElement("version", content=self.version)
110        self.send(response)
111
112        iq.handled = True
113
114    def getDiscoInfo(self, requestor, target, node):
115        info = set()
116
117        if not node:
118            from wokkel import disco
119            info.add(disco.DiscoFeature(NS_VERSION))
120
121        return defer.succeed(info)
122
123    def getDiscoItems(self, requestor, target, node):
124        return defer.succeed([])
125
126
127
128class XmlPipe(object):
129    """
130    XML stream pipe.
131
132    Connects two objects that communicate stanzas through an XML stream like
133    interface. Each of the ends of the pipe (sink and source) can be used to
134    send XML stanzas to the other side, or add observers to process XML stanzas
135    that were sent from the other side.
136
137    XML pipes are usually used in place of regular XML streams that are
138    transported over TCP. This is the reason for the use of the names source
139    and sink for both ends of the pipe. The source side corresponds with the
140    entity that initiated the TCP connection, whereas the sink corresponds with
141    the entity that accepts that connection. In this object, though, the source
142    and sink are treated equally.
143
144    Unlike Jabber
145    L{XmlStream<twisted.words.protocols.jabber.xmlstream.XmlStream>}s, the sink
146    and source objects are assumed to represent an eternal connected and
147    initialized XML stream. As such, events corresponding to connection,
148    disconnection, initialization and stream errors are not dispatched or
149    processed.
150
151    @ivar source: Source XML stream.
152    @ivar sink: Sink XML stream.
153    """
154
155    def __init__(self):
156        self.source = utility.EventDispatcher()
157        self.sink = utility.EventDispatcher()
158        self.source.send = lambda obj: self.sink.dispatch(obj)
159        self.sink.send = lambda obj: self.source.dispatch(obj)
160
161
162
163class Stanza(object):
164    """
165    Abstract representation of a stanza.
166
167    @ivar sender: The sending entity.
168    @type sender: L{jid.JID}
169    @ivar recipient: The receiving entity.
170    @type recipient: L{jid.JID}
171    """
172
173    recipient = None
174    sender = None
175    stanzaKind = None
176    stanzaID = None
177    stanzaType = None
178
179    def __init__(self, recipient=None, sender=None):
180        self.recipient = recipient
181        self.sender = sender
182
183
184    @classmethod
185    def fromElement(Class, element):
186        """
187        Create a stanza from a L{domish.Element}.
188        """
189        stanza = Class()
190        stanza.parseElement(element)
191        return stanza
192
193
194    def parseElement(self, element):
195        """
196        Parse the stanza element.
197
198        This is called with the stanza's element when a L{Stanza} is
199        created using L{fromElement}. It parses the stanza's core attributes
200        (addressing, type and id), strips the namespace from the stanza
201        element for easier transport across streams and passes on
202        child elements for further parsing.
203
204        Child element parsers are defined by providing a C{childParsers}
205        attribute on a subclass, as a mapping from (URI, name) to the name
206        of the handler on C{self}. C{parseElement} will accumulate
207        C{childParsers} from its class hierarchy, iterate over the child
208        elements and pass it to matching handlers based on the child element's
209        URI and name. The special key of C{None} can be used to pass all
210        child elements to.
211        """
212        if element.hasAttribute('from'):
213            self.sender = jid.internJID(element['from'])
214        if element.hasAttribute('to'):
215            self.recipient = jid.internJID(element['to'])
216        self.stanzaType = element.getAttribute('type')
217        self.stanzaID = element.getAttribute('id')
218
219        # Save element
220        stripNamespace(element)
221        self.element = element
222
223        # accumulate all childHandlers in the class hierarchy of Class
224        handlers = {}
225        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
226
227        for child in element.elements():
228            try:
229                handler = handlers[child.uri, child.name]
230            except KeyError:
231                try:
232                    handler = handlers[None]
233                except KeyError:
234                    continue
235
236            getattr(self, handler)(child)
237
238
239    def toElement(self):
240        element = domish.Element((None, self.stanzaKind))
241        if self.sender is not None:
242            element['from'] = self.sender.full()
243        if self.recipient is not None:
244            element['to'] = self.recipient.full()
245        if self.stanzaType:
246            element['type'] = self.stanzaType
247        if self.stanzaID:
248            element['id'] = self.stanzaID
249        return element
250
251
252
253class ErrorStanza(Stanza):
254
255    def parseElement(self, element):
256        Stanza.parseElement(self, element)
257        self.exception = error.exceptionFromStanza(element)
258
259
260
261class Request(Stanza):
262    """
263    IQ request stanza.
264
265    This is a base class for IQ get or set stanzas, to be used with
266    L{wokkel.subprotocols.StreamManager.request}.
267    """
268
269    stanzaKind = 'iq'
270    stanzaType = 'get'
271    timeout = None
272
273    childParsers = {None: 'parseRequest'}
274
275    def __init__(self, recipient=None, sender=None, stanzaType='get'):
276        Stanza.__init__(self, recipient=recipient, sender=sender)
277        self.stanzaType = stanzaType
278
279
280    def parseRequest(self, element):
281        """
282        Called with the request's child element for parsing.
283
284        When a request instance is created using L{fromElement}, this method
285        is called with the child element of the iq. Override this method for
286        parsing the request's payload.
287        """
288
289
290    def toElement(self):
291        element = Stanza.toElement(self)
292
293        if not self.stanzaID:
294            element.addUniqueId()
295            self.stanzaID = element['id']
296
297        return element
298
299
300
301class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
302    protocol = xmlstream.XmlStream
303
304    def __init__(self, authenticator):
305        BootstrapMixin.__init__(self)
306
307        self.authenticator = authenticator
308
309        deferred = defer.Deferred()
310        self.deferred = deferred
311        self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback)
312        self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
313
314
315    def buildProtocol(self, addr):
316        """
317        Create an instance of XmlStream.
318
319        A new authenticator instance will be created and passed to the new
320        XmlStream. Registered bootstrap event observers are installed as well.
321        """
322        xs = self.protocol(self.authenticator)
323        xs.factory = self
324        self.installBootstraps(xs)
325        return xs
326
327
328    def clientConnectionFailed(self, connector, reason):
329        self.deferred.errback(reason)
Note: See TracBrowser for help on using the repository browser.