source: wokkel/generic.py @ 103:e17a30fe7144

Last change on this file since 103:e17a30fe7144 was 103:e17a30fe7144, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Reimplement _DiscoRequest as generic.Request.

Author: ralphm, Goffi.
Fixes: #73.

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