OLD | NEW |
| (Empty) |
1 # Copyright (c) 2001-2007 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 class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer): | |
106 """ | |
107 Stream initializer that performs SASL authentication. | |
108 | |
109 The supported mechanisms by this initializer are C{DIGEST-MD5} and C{PLAIN} | |
110 which are attemped in that order. | |
111 """ | |
112 feature = (NS_XMPP_SASL, 'mechanisms') | |
113 _deferred = None | |
114 | |
115 def setMechanism(self): | |
116 """ | |
117 Select and setup authentication mechanism. | |
118 | |
119 Uses the authenticator's C{jid} and C{password} attribute for the | |
120 authentication credentials. If no supported SASL mechanisms are | |
121 advertized by the receiving party, a failing deferred is returned with | |
122 a L{SASLNoAcceptableMechanism} exception. | |
123 """ | |
124 | |
125 jid = self.xmlstream.authenticator.jid | |
126 password = self.xmlstream.authenticator.password | |
127 | |
128 mechanisms = get_mechanisms(self.xmlstream) | |
129 if 'DIGEST-MD5' in mechanisms: | |
130 self.mechanism = sasl_mechanisms.DigestMD5('xmpp', jid.host, None, | |
131 jid.user, password) | |
132 elif 'PLAIN' in mechanisms: | |
133 self.mechanism = sasl_mechanisms.Plain(None, jid.user, password) | |
134 else: | |
135 raise SASLNoAcceptableMechanism() | |
136 | |
137 def start(self): | |
138 """ | |
139 Start SASL authentication exchange. | |
140 """ | |
141 | |
142 self.setMechanism() | |
143 self._deferred = defer.Deferred() | |
144 self.xmlstream.addObserver('/challenge', self.onChallenge) | |
145 self.xmlstream.addOnetimeObserver('/success', self.onSuccess) | |
146 self.xmlstream.addOnetimeObserver('/failure', self.onFailure) | |
147 self.sendAuth(self.mechanism.getInitialResponse()) | |
148 return self._deferred | |
149 | |
150 def sendAuth(self, data=None): | |
151 """ | |
152 Initiate authentication protocol exchange. | |
153 | |
154 If an initial client response is given in C{data}, it will be | |
155 sent along. | |
156 | |
157 @param data: initial client response. | |
158 @type data: L{str} or L{None}. | |
159 """ | |
160 | |
161 auth = domish.Element((NS_XMPP_SASL, 'auth')) | |
162 auth['mechanism'] = self.mechanism.name | |
163 if data is not None: | |
164 auth.addContent(b64encode(data) or '=') | |
165 self.xmlstream.send(auth) | |
166 | |
167 def sendResponse(self, data=''): | |
168 """ | |
169 Send response to a challenge. | |
170 | |
171 @param data: client response. | |
172 @type data: L{str}. | |
173 """ | |
174 | |
175 response = domish.Element((NS_XMPP_SASL, 'response')) | |
176 if data: | |
177 response.addContent(b64encode(data)) | |
178 self.xmlstream.send(response) | |
179 | |
180 def onChallenge(self, element): | |
181 """ | |
182 Parse challenge and send response from the mechanism. | |
183 | |
184 @param element: the challenge protocol element. | |
185 @type element: L{domish.Element}. | |
186 """ | |
187 | |
188 try: | |
189 challenge = fromBase64(str(element)) | |
190 except SASLIncorrectEncodingError: | |
191 self._deferred.errback() | |
192 else: | |
193 self.sendResponse(self.mechanism.getResponse(challenge)) | |
194 | |
195 def onSuccess(self, success): | |
196 """ | |
197 Clean up observers, reset the XML stream and send a new header. | |
198 | |
199 @param success: the success protocol element. For now unused, but | |
200 could hold additional data. | |
201 @type success: L{domish.Element} | |
202 """ | |
203 | |
204 self.xmlstream.removeObserver('/challenge', self.onChallenge) | |
205 self.xmlstream.removeObserver('/failure', self.onFailure) | |
206 self.xmlstream.reset() | |
207 self.xmlstream.sendHeader() | |
208 self._deferred.callback(xmlstream.Reset) | |
209 | |
210 def onFailure(self, failure): | |
211 """ | |
212 Clean up observers, parse the failure and errback the deferred. | |
213 | |
214 @param failure: the failure protocol element. Holds details on | |
215 the error condition. | |
216 @type failure: L{domish.Element} | |
217 """ | |
218 | |
219 self.xmlstream.removeObserver('/challenge', self.onChallenge) | |
220 self.xmlstream.removeObserver('/success', self.onSuccess) | |
221 try: | |
222 condition = failure.firstChildElement().name | |
223 except AttributeError: | |
224 condition = None | |
225 self._deferred.errback(SASLAuthError(condition)) | |
OLD | NEW |