1
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
XMPP-specific SASL profile.
9
from twisted.internet import defer
10
from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream
11
from twisted.words.xish import domish
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.
18
from base64 import b64decode, b64encode
23
return "".join(base64.encodestring(s).split("\n"))
25
b64decode = base64.decodestring
27
NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
29
def get_mechanisms(xs):
31
Parse the SASL feature to extract the available mechanism names.
34
for element in xs.features[(NS_XMPP_SASL, 'mechanisms')].elements():
35
if element.name == 'mechanism':
36
mechanisms.append(str(element))
41
class SASLError(Exception):
47
class SASLNoAcceptableMechanism(SASLError):
49
The server did not present an acceptable SASL mechanism.
53
class SASLAuthError(SASLError):
55
SASL Authentication failed.
57
def __init__(self, condition=None):
58
self.condition = condition
62
return "SASLAuthError with condition %r" % self.condition
65
class SASLIncorrectEncodingError(SASLError):
67
SASL base64 encoding was incorrect.
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}.
73
This exception is raised whenever the encoded string does not adhere
74
to these additional restrictions or when the decoding itself fails.
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
84
base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$")
88
Decode base64 encoded string.
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.
97
if base64Pattern.match(s) is None:
98
raise SASLIncorrectEncodingError()
103
raise SASLIncorrectEncodingError(str(e))
107
class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer):
109
Stream initializer that performs SASL authentication.
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.
118
feature = (NS_XMPP_SASL, 'mechanisms')
121
def setMechanism(self):
123
Select and setup authentication mechanism.
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.
131
jid = self.xmlstream.authenticator.jid
132
password = self.xmlstream.authenticator.password
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,
139
elif 'PLAIN' in mechanisms:
140
self.mechanism = sasl_mechanisms.Plain(None, jid.user, password)
142
raise SASLNoAcceptableMechanism()
144
if 'ANONYMOUS' in mechanisms:
145
self.mechanism = sasl_mechanisms.Anonymous()
147
raise SASLNoAcceptableMechanism()
152
Start SASL authentication exchange.
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
164
def sendAuth(self, data=None):
166
Initiate authentication protocol exchange.
168
If an initial client response is given in C{data}, it will be
171
@param data: initial client response.
172
@type data: L{str} or L{None}.
175
auth = domish.Element((NS_XMPP_SASL, 'auth'))
176
auth['mechanism'] = self.mechanism.name
178
auth.addContent(b64encode(data) or '=')
179
self.xmlstream.send(auth)
182
def sendResponse(self, data=''):
184
Send response to a challenge.
186
@param data: client response.
190
response = domish.Element((NS_XMPP_SASL, 'response'))
192
response.addContent(b64encode(data))
193
self.xmlstream.send(response)
196
def onChallenge(self, element):
198
Parse challenge and send response from the mechanism.
200
@param element: the challenge protocol element.
201
@type element: L{domish.Element}.
205
challenge = fromBase64(str(element))
206
except SASLIncorrectEncodingError:
207
self._deferred.errback()
209
self.sendResponse(self.mechanism.getResponse(challenge))
212
def onSuccess(self, success):
214
Clean up observers, reset the XML stream and send a new header.
216
@param success: the success protocol element. For now unused, but
217
could hold additional data.
218
@type success: L{domish.Element}
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)
228
def onFailure(self, failure):
230
Clean up observers, parse the failure and errback the deferred.
232
@param failure: the failure protocol element. Holds details on
234
@type failure: L{domish.Element}
237
self.xmlstream.removeObserver('/challenge', self.onChallenge)
238
self.xmlstream.removeObserver('/success', self.onSuccess)
240
condition = failure.firstChildElement().name
241
except AttributeError:
243
self._deferred.errback(SASLAuthError(condition))