~0x44/nova/extdoc

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/words/protocols/jabber/sasl.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
 
2
# See LICENSE for details.
 
3
 
 
4
"""
 
5
XMPP-specific SASL profile.
 
6
"""
 
7
 
 
8
import re
 
9
from twisted.internet import defer
 
10
from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream
 
11
from twisted.words.xish import domish
 
12
 
 
13
# The b64decode and b64encode functions from the base64 module are new in
 
14
# Python 2.4. For Python 2.3 compatibility, the legacy interface is used while
 
15
# working around MIMEisms.
 
16
 
 
17
try:
 
18
    from base64 import b64decode, b64encode
 
19
except ImportError:
 
20
    import base64
 
21
 
 
22
    def b64encode(s):
 
23
        return "".join(base64.encodestring(s).split("\n"))
 
24
 
 
25
    b64decode = base64.decodestring
 
26
 
 
27
NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
 
28
 
 
29
def get_mechanisms(xs):
 
30
    """
 
31
    Parse the SASL feature to extract the available mechanism names.
 
32
    """
 
33
    mechanisms = []
 
34
    for element in xs.features[(NS_XMPP_SASL, 'mechanisms')].elements():
 
35
        if element.name == 'mechanism':
 
36
            mechanisms.append(str(element))
 
37
 
 
38
    return mechanisms
 
39
 
 
40
 
 
41
class SASLError(Exception):
 
42
    """
 
43
    SASL base exception.
 
44
    """
 
45
 
 
46
 
 
47
class SASLNoAcceptableMechanism(SASLError):
 
48
    """
 
49
    The server did not present an acceptable SASL mechanism.
 
50
    """
 
51
 
 
52
 
 
53
class SASLAuthError(SASLError):
 
54
    """
 
55
    SASL Authentication failed.
 
56
    """
 
57
    def __init__(self, condition=None):
 
58
        self.condition = condition
 
59
 
 
60
 
 
61
    def __str__(self):
 
62
        return "SASLAuthError with condition %r" % self.condition
 
63
 
 
64
 
 
65
class SASLIncorrectEncodingError(SASLError):
 
66
    """
 
67
    SASL base64 encoding was incorrect.
 
68
 
 
69
    RFC 3920 specifies that any characters not in the base64 alphabet
 
70
    and padding characters present elsewhere than at the end of the string
 
71
    MUST be rejected. See also L{fromBase64}.
 
72
 
 
73
    This exception is raised whenever the encoded string does not adhere
 
74
    to these additional restrictions or when the decoding itself fails.
 
75
 
 
76
    The recommended behaviour for so-called receiving entities (like servers in
 
77
    client-to-server connections, see RFC 3920 for terminology) is to fail the
 
78
    SASL negotiation with a C{'incorrect-encoding'} condition. For initiating
 
79
    entities, one should assume the receiving entity to be either buggy or
 
80
    malevolent. The stream should be terminated and reconnecting is not
 
81
    advised.
 
82
    """
 
83
 
 
84
base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$")
 
85
 
 
86
def fromBase64(s):
 
87
    """
 
88
    Decode base64 encoded string.
 
89
 
 
90
    This helper performs regular decoding of a base64 encoded string, but also
 
91
    rejects any characters that are not in the base64 alphabet and padding
 
92
    occurring elsewhere from the last or last two characters, as specified in
 
93
    section 14.9 of RFC 3920. This safeguards against various attack vectors
 
94
    among which the creation of a covert channel that "leaks" information.
 
95
    """
 
96
 
 
97
    if base64Pattern.match(s) is None:
 
98
        raise SASLIncorrectEncodingError()
 
99
 
 
100
    try:
 
101
        return b64decode(s)
 
102
    except Exception, e:
 
103
        raise SASLIncorrectEncodingError(str(e))
 
104
 
 
105
 
 
106
 
 
107
class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer):
 
108
    """
 
109
    Stream initializer that performs SASL authentication.
 
110
 
 
111
    The supported mechanisms by this initializer are C{DIGEST-MD5}, C{PLAIN}
 
112
    and C{ANONYMOUS}. The C{ANONYMOUS} SASL mechanism is used when the JID, set
 
113
    on the authenticator, does not have a localpart (username), requesting an
 
114
    anonymous session where the username is generated by the server.
 
115
    Otherwise, C{DIGEST-MD5} and C{PLAIN} are attempted, in that order.
 
116
    """
 
117
 
 
118
    feature = (NS_XMPP_SASL, 'mechanisms')
 
119
    _deferred = None
 
120
 
 
121
    def setMechanism(self):
 
122
        """
 
123
        Select and setup authentication mechanism.
 
124
 
 
125
        Uses the authenticator's C{jid} and C{password} attribute for the
 
126
        authentication credentials. If no supported SASL mechanisms are
 
127
        advertized by the receiving party, a failing deferred is returned with
 
128
        a L{SASLNoAcceptableMechanism} exception.
 
129
        """
 
130
 
 
131
        jid = self.xmlstream.authenticator.jid
 
132
        password = self.xmlstream.authenticator.password
 
133
 
 
134
        mechanisms = get_mechanisms(self.xmlstream)
 
135
        if jid.user is not None:
 
136
            if 'DIGEST-MD5' in mechanisms:
 
137
                self.mechanism = sasl_mechanisms.DigestMD5('xmpp', jid.host, None,
 
138
                                                           jid.user, password)
 
139
            elif 'PLAIN' in mechanisms:
 
140
                self.mechanism = sasl_mechanisms.Plain(None, jid.user, password)
 
141
            else:
 
142
                raise SASLNoAcceptableMechanism()
 
143
        else:
 
144
            if 'ANONYMOUS' in mechanisms:
 
145
                self.mechanism = sasl_mechanisms.Anonymous()
 
146
            else:
 
147
                raise SASLNoAcceptableMechanism()
 
148
 
 
149
 
 
150
    def start(self):
 
151
        """
 
152
        Start SASL authentication exchange.
 
153
        """
 
154
 
 
155
        self.setMechanism()
 
156
        self._deferred = defer.Deferred()
 
157
        self.xmlstream.addObserver('/challenge', self.onChallenge)
 
158
        self.xmlstream.addOnetimeObserver('/success', self.onSuccess)
 
159
        self.xmlstream.addOnetimeObserver('/failure', self.onFailure)
 
160
        self.sendAuth(self.mechanism.getInitialResponse())
 
161
        return self._deferred
 
162
 
 
163
 
 
164
    def sendAuth(self, data=None):
 
165
        """
 
166
        Initiate authentication protocol exchange.
 
167
 
 
168
        If an initial client response is given in C{data}, it will be
 
169
        sent along.
 
170
 
 
171
        @param data: initial client response.
 
172
        @type data: L{str} or L{None}.
 
173
        """
 
174
 
 
175
        auth = domish.Element((NS_XMPP_SASL, 'auth'))
 
176
        auth['mechanism'] = self.mechanism.name
 
177
        if data is not None:
 
178
            auth.addContent(b64encode(data) or '=')
 
179
        self.xmlstream.send(auth)
 
180
 
 
181
 
 
182
    def sendResponse(self, data=''):
 
183
        """
 
184
        Send response to a challenge.
 
185
 
 
186
        @param data: client response.
 
187
        @type data: L{str}.
 
188
        """
 
189
 
 
190
        response = domish.Element((NS_XMPP_SASL, 'response'))
 
191
        if data:
 
192
            response.addContent(b64encode(data))
 
193
        self.xmlstream.send(response)
 
194
 
 
195
 
 
196
    def onChallenge(self, element):
 
197
        """
 
198
        Parse challenge and send response from the mechanism.
 
199
 
 
200
        @param element: the challenge protocol element.
 
201
        @type element: L{domish.Element}.
 
202
        """
 
203
 
 
204
        try:
 
205
            challenge = fromBase64(str(element))
 
206
        except SASLIncorrectEncodingError:
 
207
            self._deferred.errback()
 
208
        else:
 
209
            self.sendResponse(self.mechanism.getResponse(challenge))
 
210
 
 
211
 
 
212
    def onSuccess(self, success):
 
213
        """
 
214
        Clean up observers, reset the XML stream and send a new header.
 
215
 
 
216
        @param success: the success protocol element. For now unused, but
 
217
                        could hold additional data.
 
218
        @type success: L{domish.Element}
 
219
        """
 
220
 
 
221
        self.xmlstream.removeObserver('/challenge', self.onChallenge)
 
222
        self.xmlstream.removeObserver('/failure', self.onFailure)
 
223
        self.xmlstream.reset()
 
224
        self.xmlstream.sendHeader()
 
225
        self._deferred.callback(xmlstream.Reset)
 
226
 
 
227
 
 
228
    def onFailure(self, failure):
 
229
        """
 
230
        Clean up observers, parse the failure and errback the deferred.
 
231
 
 
232
        @param failure: the failure protocol element. Holds details on
 
233
                        the error condition.
 
234
        @type failure: L{domish.Element}
 
235
        """
 
236
 
 
237
        self.xmlstream.removeObserver('/challenge', self.onChallenge)
 
238
        self.xmlstream.removeObserver('/success', self.onSuccess)
 
239
        try:
 
240
            condition = failure.firstChildElement().name
 
241
        except AttributeError:
 
242
            condition = None
 
243
        self._deferred.errback(SASLAuthError(condition))