source: wokkel/generic.py @ 178:37f36ea93838

Last change on this file since 178:37f36ea93838 was 178:37f36ea93838, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Properly encode IDN domain names for SRV lookups.

Before Twisted 12.3.0, the SRV lookup done for outgoing
connections succeeded if passed a unicode string with all-ASCII
code points. A recent change made the DNS code more strict, and
only byte string are accepted as domain name. See
http://twistedmatrix.com/trac/ticket/6245 for details.

This change makes sure domain names are encoded to their ASCII
Compatible Equivalent (ACE) version before passing the resulting
byte string to twisted.names.srvconnect.SRVConnector, as per
RFC 3490.

Note that while connecting to servers with an IDN domain name
now works properly, authentication using MD5-Digest SASL mechanism
will fail until http://twistedmatrix.com/trac/ticket/5066 has been
resolved.

Fixes: #77.

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