source: wokkel/generic.py @ 102:039149921acf

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

Add base class for IQ get/set request stanzas: Request.

  • 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 import disco
24from wokkel.iwokkel import IDisco
25from wokkel.subprotocols import XMPPHandler
26
27IQ_GET = '/iq[@type="get"]'
28IQ_SET = '/iq[@type="set"]'
29
30NS_VERSION = 'jabber:iq:version'
31VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
32
33def parseXml(string):
34    """
35    Parse serialized XML into a DOM structure.
36
37    @param string: The serialized XML to be parsed, UTF-8 encoded.
38    @type string: C{str}.
39    @return: The DOM structure, or C{None} on empty or incomplete input.
40    @rtype: L{domish.Element}
41    """
42    roots = []
43    results = []
44    elementStream = domish.elementStream()
45    elementStream.DocumentStartEvent = roots.append
46    elementStream.ElementEvent = lambda elem: roots[0].addChild(elem)
47    elementStream.DocumentEndEvent = lambda: results.append(roots[0])
48    elementStream.parse(string)
49    return results and results[0] or None
50
51
52
53def stripNamespace(rootElement):
54    namespace = rootElement.uri
55
56    def strip(element):
57        if element.uri == namespace:
58            element.uri = None
59            if element.defaultUri == namespace:
60                element.defaultUri = None
61            for child in element.elements():
62                strip(child)
63
64    if namespace is not None:
65        strip(rootElement)
66
67    return rootElement
68
69
70
71class FallbackHandler(XMPPHandler):
72    """
73    XMPP subprotocol handler that catches unhandled iq requests.
74
75    Unhandled iq requests are replied to with a service-unavailable stanza
76    error.
77    """
78
79    def connectionInitialized(self):
80        self.xmlstream.addObserver(IQ_SET, self.iqFallback, -1)
81        self.xmlstream.addObserver(IQ_GET, self.iqFallback, -1)
82
83    def iqFallback(self, iq):
84        if iq.handled == True:
85            return
86
87        reply = error.StanzaError('service-unavailable')
88        self.xmlstream.send(reply.toResponse(iq))
89
90
91
92class VersionHandler(XMPPHandler):
93    """
94    XMPP subprotocol handler for XMPP Software Version.
95
96    This protocol is described in
97    U{XEP-0092<http://www.xmpp.org/extensions/xep-0092.html>}.
98    """
99
100    implements(IDisco)
101
102    def __init__(self, name, version):
103        self.name = name
104        self.version = version
105
106    def connectionInitialized(self):
107        self.xmlstream.addObserver(VERSION, self.onVersion)
108
109    def onVersion(self, iq):
110        response = toResponse(iq, "result")
111
112        query = response.addElement((NS_VERSION, "query"))
113        name = query.addElement("name", content=self.name)
114        version = query.addElement("version", content=self.version)
115        self.send(response)
116
117        iq.handled = True
118
119    def getDiscoInfo(self, requestor, target, node):
120        info = set()
121
122        if not node:
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    stanzaKind = None
178    stanzaID = None
179    stanzaType = None
180
181    def __init__(self, recipient=None, sender=None):
182        self.recipient = recipient
183        self.sender = sender
184
185
186    @classmethod
187    def fromElement(Class, element):
188        stanza = Class()
189        stanza.parseElement(element)
190        return stanza
191
192
193    def parseElement(self, element):
194        if element.hasAttribute('from'):
195            self.sender = jid.internJID(element['from'])
196        if element.hasAttribute('to'):
197            self.recipient = jid.internJID(element['to'])
198        self.stanzaType = element.getAttribute('type')
199        self.stanzaID = element.getAttribute('id')
200
201        # Save element
202        stripNamespace(element)
203        self.element = element
204
205        # accumulate all childHandlers in the class hierarchy of Class
206        handlers = {}
207        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
208
209        for child in element.elements():
210            try:
211                handler = handlers[child.uri, child.name]
212            except KeyError:
213                pass
214            else:
215                getattr(self, handler)(child)
216
217
218    def toElement(self):
219        element = domish.Element((None, self.stanzaKind))
220        if self.sender is not None:
221            element['from'] = self.sender.full()
222        if self.recipient is not None:
223            element['to'] = self.recipient.full()
224        if self.stanzaType:
225            element['type'] = self.stanzaType
226        if self.stanzaID:
227            element['id'] = self.stanzaID
228        return element
229
230
231
232class ErrorStanza(Stanza):
233
234    def parseElement(self, element):
235        Stanza.parseElement(self, element)
236        self.exception = error.exceptionFromStanza(element)
237
238
239class Request(Stanza):
240    """
241    IQ request stanza.
242
243    This is a base class for IQ get or set stanzas, to be used with
244    L{wokkel.subprotocols.StreamManager.request}.
245    """
246
247    stanzaKind = 'iq'
248    stanzaType = 'get'
249    timeout = None
250
251    def __init__(self, recipient=None, sender=None, stanzaType='get'):
252        Stanza.__init__(self, recipient=recipient, sender=sender)
253        self.stanzaType = stanzaType
254
255
256    def toElement(self):
257        element = Stanza.toElement(self)
258
259        if not self.stanzaID:
260            element.addUniqueId()
261            self.stanzaID = element['id']
262
263        return element
264
265
266
267class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
268    protocol = xmlstream.XmlStream
269
270    def __init__(self, authenticator):
271        BootstrapMixin.__init__(self)
272
273        self.authenticator = authenticator
274
275        deferred = defer.Deferred()
276        self.deferred = deferred
277        self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback)
278        self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
279
280
281    def buildProtocol(self, addr):
282        """
283        Create an instance of XmlStream.
284
285        A new authenticator instance will be created and passed to the new
286        XmlStream. Registered bootstrap event observers are installed as well.
287        """
288        xs = self.protocol(self.authenticator)
289        xs.factory = self
290        self.installBootstraps(xs)
291        return xs
292
293
294    def clientConnectionFailed(self, connector, reason):
295        self.deferred.errback(reason)
Note: See TracBrowser for help on using the repository browser.