source: wokkel/generic.py @ 165:76a61f5aa343

Last change on this file since 165:76a61f5aa343 was 165:76a61f5aa343, checked in by Ralph Meijer <ralphm@…>, 11 years ago

Cleanups leading up to Wokkel 0.7.0.

As we now depend on Twisted 10.0.0 or higher, the following classes and
interfaces were deprecated:

This also resolves all Pyflakes warnings, changes links for www.xmpp.org to
xmpp.org and fixes the copyright notice in LICENSE to include 2012.

  • Property exe set to *
File size: 8.1 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        stanza = Class()
187        stanza.parseElement(element)
188        return stanza
189
190
191    def parseElement(self, element):
192        if element.hasAttribute('from'):
193            self.sender = jid.internJID(element['from'])
194        if element.hasAttribute('to'):
195            self.recipient = jid.internJID(element['to'])
196        self.stanzaType = element.getAttribute('type')
197        self.stanzaID = element.getAttribute('id')
198
199        # Save element
200        stripNamespace(element)
201        self.element = element
202
203        # accumulate all childHandlers in the class hierarchy of Class
204        handlers = {}
205        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
206
207        for child in element.elements():
208            try:
209                handler = handlers[child.uri, child.name]
210            except KeyError:
211                pass
212            else:
213                getattr(self, handler)(child)
214
215
216    def toElement(self):
217        element = domish.Element((None, self.stanzaKind))
218        if self.sender is not None:
219            element['from'] = self.sender.full()
220        if self.recipient is not None:
221            element['to'] = self.recipient.full()
222        if self.stanzaType:
223            element['type'] = self.stanzaType
224        if self.stanzaID:
225            element['id'] = self.stanzaID
226        return element
227
228
229
230class ErrorStanza(Stanza):
231
232    def parseElement(self, element):
233        Stanza.parseElement(self, element)
234        self.exception = error.exceptionFromStanza(element)
235
236
237class Request(Stanza):
238    """
239    IQ request stanza.
240
241    This is a base class for IQ get or set stanzas, to be used with
242    L{wokkel.subprotocols.StreamManager.request}.
243    """
244
245    stanzaKind = 'iq'
246    stanzaType = 'get'
247    timeout = None
248
249    def __init__(self, recipient=None, sender=None, stanzaType='get'):
250        Stanza.__init__(self, recipient=recipient, sender=sender)
251        self.stanzaType = stanzaType
252
253
254    def toElement(self):
255        element = Stanza.toElement(self)
256
257        if not self.stanzaID:
258            element.addUniqueId()
259            self.stanzaID = element['id']
260
261        return element
262
263
264
265class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
266    protocol = xmlstream.XmlStream
267
268    def __init__(self, authenticator):
269        BootstrapMixin.__init__(self)
270
271        self.authenticator = authenticator
272
273        deferred = defer.Deferred()
274        self.deferred = deferred
275        self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback)
276        self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
277
278
279    def buildProtocol(self, addr):
280        """
281        Create an instance of XmlStream.
282
283        A new authenticator instance will be created and passed to the new
284        XmlStream. Registered bootstrap event observers are installed as well.
285        """
286        xs = self.protocol(self.authenticator)
287        xs.factory = self
288        self.installBootstraps(xs)
289        return xs
290
291
292    def clientConnectionFailed(self, connector, reason):
293        self.deferred.errback(reason)
Note: See TracBrowser for help on using the repository browser.