| 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 |