source: wokkel/generic.py @ 246:388bfe3e4ea7

Last change on this file since 246:388bfe3e4ea7 was 246:388bfe3e4ea7, checked in by Ralph Meijer <ralphm@…>, 15 months ago

Prepare 18.0.0 final

File size: 10.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 __future__ import division, absolute_import
11
12from zope.interface import implementer
13
14from twisted.internet import defer, protocol
15from twisted.python import reflect
16from twisted.python.deprecate import deprecated
17from twisted.words.protocols.jabber import error, jid, xmlstream
18from twisted.words.protocols.jabber.xmlstream import toResponse
19from twisted.words.xish import domish, utility
20from twisted.words.xish.xmlstream import BootstrapMixin
21
22from incremental import Version
23
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
92@implementer(IDisco)
93class VersionHandler(XMPPHandler):
94    """
95    XMPP subprotocol handler for XMPP Software Version.
96
97    This protocol is described in
98    U{XEP-0092<http://xmpp.org/extensions/xep-0092.html>}.
99    """
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        query.addElement("name", content=self.name)
113        query.addElement("version", content=self.version)
114        self.send(response)
115
116        iq.handled = True
117
118    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
119        info = set()
120
121        if not nodeIdentifier:
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, nodeIdentifier=''):
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        """
191        Create a stanza from a L{domish.Element}.
192        """
193        stanza = Class()
194        stanza.parseElement(element)
195        return stanza
196
197
198    def parseElement(self, element):
199        """
200        Parse the stanza element.
201
202        This is called with the stanza's element when a L{Stanza} is
203        created using L{fromElement}. It parses the stanza's core attributes
204        (addressing, type and id), strips the namespace from the stanza
205        element for easier transport across streams and passes on
206        child elements for further parsing.
207
208        Child element parsers are defined by providing a C{childParsers}
209        attribute on a subclass, as a mapping from (URI, name) to the name
210        of the handler on C{self}. C{parseElement} will accumulate
211        C{childParsers} from its class hierarchy, iterate over the child
212        elements and pass it to matching handlers based on the child element's
213        URI and name. The special key of C{None} can be used to pass all
214        child elements to.
215        """
216        if element.hasAttribute('from'):
217            self.sender = jid.internJID(element['from'])
218        if element.hasAttribute('to'):
219            self.recipient = jid.internJID(element['to'])
220        self.stanzaType = element.getAttribute('type')
221        self.stanzaID = element.getAttribute('id')
222
223        # Save element
224        stripNamespace(element)
225        self.element = element
226
227        # accumulate all childHandlers in the class hierarchy of Class
228        handlers = {}
229        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
230
231        for child in element.elements():
232            try:
233                handler = handlers[child.uri, child.name]
234            except KeyError:
235                try:
236                    handler = handlers[None]
237                except KeyError:
238                    continue
239
240            getattr(self, handler)(child)
241
242
243    def toElement(self):
244        element = domish.Element((None, self.stanzaKind))
245        if self.sender is not None:
246            element['from'] = self.sender.full()
247        if self.recipient is not None:
248            element['to'] = self.recipient.full()
249        if self.stanzaType:
250            element['type'] = self.stanzaType
251        if self.stanzaID:
252            element['id'] = self.stanzaID
253        return element
254
255
256
257class ErrorStanza(Stanza):
258
259    def parseElement(self, element):
260        Stanza.parseElement(self, element)
261        self.exception = error.exceptionFromStanza(element)
262
263
264
265class Request(Stanza):
266    """
267    IQ request stanza.
268
269    This is a base class for IQ get or set stanzas, to be used with
270    L{wokkel.subprotocols.StreamManager.request}.
271    """
272
273    stanzaKind = 'iq'
274    stanzaType = 'get'
275    timeout = None
276
277    childParsers = {None: 'parseRequest'}
278
279    def __init__(self, recipient=None, sender=None, stanzaType=None):
280        Stanza.__init__(self, recipient=recipient, sender=sender)
281        if stanzaType is not None:
282            self.stanzaType = stanzaType
283
284
285    def parseRequest(self, element):
286        """
287        Called with the request's child element for parsing.
288
289        When a request instance is created using L{fromElement}, this method
290        is called with the child element of the iq. Override this method for
291        parsing the request's payload.
292        """
293
294
295    def toElement(self):
296        element = Stanza.toElement(self)
297
298        if not self.stanzaID:
299            element.addUniqueId()
300            self.stanzaID = element['id']
301
302        return element
303
304
305
306class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
307    protocol = xmlstream.XmlStream
308
309    def __init__(self, authenticator):
310        BootstrapMixin.__init__(self)
311
312        self.authenticator = authenticator
313
314        deferred = defer.Deferred()
315        self.deferred = deferred
316        self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback)
317        self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
318
319
320    def buildProtocol(self, addr):
321        """
322        Create an instance of XmlStream.
323
324        A new authenticator instance will be created and passed to the new
325        XmlStream. Registered bootstrap event observers are installed as well.
326        """
327        xs = self.protocol(self.authenticator)
328        xs.factory = self
329        self.installBootstraps(xs)
330        return xs
331
332
333    def clientConnectionFailed(self, connector, reason):
334        self.deferred.errback(reason)
335
336
337
338@deprecated(Version("wokkel", 18, 0, 0), "unicode.encode('idna')")
339def prepareIDNName(name):
340    """
341    Encode a unicode IDN Domain Name into its ACE equivalent.
342
343    This will encode the domain labels, separated by allowed dot code points,
344    to their ASCII Compatible Encoding (ACE) equivalent, using punycode. The
345    result is an ASCII byte string of the encoded labels, separated by the
346    standard full stop.
347    """
348    return name.encode('idna')
Note: See TracBrowser for help on using the repository browser.