| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.words.test.test_jabberclient -*- | |
| 2 # | |
| 3 # Copyright (c) 2001-2005 Twisted Matrix Laboratories. | |
| 4 # See LICENSE for details. | |
| 5 | |
| 6 from twisted.internet import defer | |
| 7 from twisted.words.xish import domish, xpath, utility | |
| 8 from twisted.words.protocols.jabber import xmlstream, sasl, error | |
| 9 from twisted.words.protocols.jabber.jid import JID | |
| 10 | |
| 11 NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams' | |
| 12 NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' | |
| 13 NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session' | |
| 14 NS_IQ_AUTH_FEATURE = 'http://jabber.org/features/iq-auth' | |
| 15 | |
| 16 DigestAuthQry = xpath.internQuery("/iq/query/digest") | |
| 17 PlaintextAuthQry = xpath.internQuery("/iq/query/password") | |
| 18 | |
| 19 def basicClientFactory(jid, secret): | |
| 20 a = BasicAuthenticator(jid, secret) | |
| 21 return xmlstream.XmlStreamFactory(a) | |
| 22 | |
| 23 class IQ(domish.Element): | |
| 24 """ | |
| 25 Wrapper for a Info/Query packet. | |
| 26 | |
| 27 This provides the necessary functionality to send IQs and get notified when | |
| 28 a result comes back. It's a subclass from L{domish.Element}, so you can use | |
| 29 the standard DOM manipulation calls to add data to the outbound request. | |
| 30 | |
| 31 @type callbacks: L{utility.CallbackList} | |
| 32 @cvar callbacks: Callback list to be notified when response comes back | |
| 33 | |
| 34 """ | |
| 35 def __init__(self, xmlstream, type = "set"): | |
| 36 """ | |
| 37 @type xmlstream: L{xmlstream.XmlStream} | |
| 38 @param xmlstream: XmlStream to use for transmission of this IQ | |
| 39 | |
| 40 @type type: L{str} | |
| 41 @param type: IQ type identifier ('get' or 'set') | |
| 42 """ | |
| 43 | |
| 44 domish.Element.__init__(self, ("jabber:client", "iq")) | |
| 45 self.addUniqueId() | |
| 46 self["type"] = type | |
| 47 self._xmlstream = xmlstream | |
| 48 self.callbacks = utility.CallbackList() | |
| 49 | |
| 50 def addCallback(self, fn, *args, **kwargs): | |
| 51 """ | |
| 52 Register a callback for notification when the IQ result is available. | |
| 53 """ | |
| 54 | |
| 55 self.callbacks.addCallback(True, fn, *args, **kwargs) | |
| 56 | |
| 57 def send(self, to = None): | |
| 58 """ | |
| 59 Call this method to send this IQ request via the associated XmlStream. | |
| 60 | |
| 61 @param to: Jabber ID of the entity to send the request to | |
| 62 @type to: L{str} | |
| 63 | |
| 64 @returns: Callback list for this IQ. Any callbacks added to this list | |
| 65 will be fired when the result comes back. | |
| 66 """ | |
| 67 if to != None: | |
| 68 self["to"] = to | |
| 69 self._xmlstream.addOnetimeObserver("/iq[@id='%s']" % self["id"], \ | |
| 70 self._resultEvent) | |
| 71 self._xmlstream.send(self) | |
| 72 | |
| 73 def _resultEvent(self, iq): | |
| 74 self.callbacks.callback(iq) | |
| 75 self.callbacks = None | |
| 76 | |
| 77 | |
| 78 | |
| 79 class IQAuthInitializer(object): | |
| 80 """ | |
| 81 Non-SASL Authentication initializer for the initiating entity. | |
| 82 | |
| 83 This protocol is defined in | |
| 84 U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>} and mainly serves for | |
| 85 compatibility with pre-XMPP-1.0 server implementations. | |
| 86 """ | |
| 87 | |
| 88 INVALID_USER_EVENT = "//event/client/basicauth/invaliduser" | |
| 89 AUTH_FAILED_EVENT = "//event/client/basicauth/authfailed" | |
| 90 | |
| 91 def __init__(self, xs): | |
| 92 self.xmlstream = xs | |
| 93 | |
| 94 | |
| 95 def initialize(self): | |
| 96 # Send request for auth fields | |
| 97 iq = xmlstream.IQ(self.xmlstream, "get") | |
| 98 iq.addElement(("jabber:iq:auth", "query")) | |
| 99 jid = self.xmlstream.authenticator.jid | |
| 100 iq.query.addElement("username", content = jid.user) | |
| 101 | |
| 102 d = iq.send() | |
| 103 d.addCallbacks(self._cbAuthQuery, self._ebAuthQuery) | |
| 104 return d | |
| 105 | |
| 106 | |
| 107 def _cbAuthQuery(self, iq): | |
| 108 jid = self.xmlstream.authenticator.jid | |
| 109 password = self.xmlstream.authenticator.password | |
| 110 | |
| 111 # Construct auth request | |
| 112 reply = xmlstream.IQ(self.xmlstream, "set") | |
| 113 reply.addElement(("jabber:iq:auth", "query")) | |
| 114 reply.query.addElement("username", content = jid.user) | |
| 115 reply.query.addElement("resource", content = jid.resource) | |
| 116 | |
| 117 # Prefer digest over plaintext | |
| 118 if DigestAuthQry.matches(iq): | |
| 119 digest = xmlstream.hashPassword(self.xmlstream.sid, password) | |
| 120 reply.query.addElement("digest", content = digest) | |
| 121 else: | |
| 122 reply.query.addElement("password", content = password) | |
| 123 | |
| 124 d = reply.send() | |
| 125 d.addCallbacks(self._cbAuth, self._ebAuth) | |
| 126 return d | |
| 127 | |
| 128 | |
| 129 def _ebAuthQuery(self, failure): | |
| 130 failure.trap(error.StanzaError) | |
| 131 e = failure.value | |
| 132 if e.condition == 'not-authorized': | |
| 133 self.xmlstream.dispatch(e.stanza, self.INVALID_USER_EVENT) | |
| 134 else: | |
| 135 self.xmlstream.dispatch(e.stanza, self.AUTH_FAILED_EVENT) | |
| 136 | |
| 137 return failure | |
| 138 | |
| 139 | |
| 140 def _cbAuth(self, iq): | |
| 141 pass | |
| 142 | |
| 143 | |
| 144 def _ebAuth(self, failure): | |
| 145 failure.trap(error.StanzaError) | |
| 146 self.xmlstream.dispatch(failure.value.stanza, self.AUTH_FAILED_EVENT) | |
| 147 return failure | |
| 148 | |
| 149 | |
| 150 | |
| 151 class BasicAuthenticator(xmlstream.ConnectAuthenticator): | |
| 152 """ | |
| 153 Authenticates an XmlStream against a Jabber server as a Client. | |
| 154 | |
| 155 This only implements non-SASL authentication, per | |
| 156 U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>}. Additionally, this | |
| 157 authenticator provides the ability to perform inline registration, per | |
| 158 U{JEP-0077<http://www.jabber.org/jeps/jep-0077.html>}. | |
| 159 | |
| 160 Under normal circumstances, the BasicAuthenticator generates the | |
| 161 L{xmlstream.STREAM_AUTHD_EVENT} once the stream has authenticated. However, | |
| 162 it can also generate other events, such as: | |
| 163 - L{INVALID_USER_EVENT} : Authentication failed, due to invalid username | |
| 164 - L{AUTH_FAILED_EVENT} : Authentication failed, due to invalid password | |
| 165 - L{REGISTER_FAILED_EVENT} : Registration failed | |
| 166 | |
| 167 If authentication fails for any reason, you can attempt to register by | |
| 168 calling the L{registerAccount} method. If the registration succeeds, a | |
| 169 L{xmlstream.STREAM_AUTHD_EVENT} will be fired. Otherwise, one of the above | |
| 170 errors will be generated (again). | |
| 171 """ | |
| 172 | |
| 173 namespace = "jabber:client" | |
| 174 | |
| 175 INVALID_USER_EVENT = IQAuthInitializer.INVALID_USER_EVENT | |
| 176 AUTH_FAILED_EVENT = IQAuthInitializer.AUTH_FAILED_EVENT | |
| 177 REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed" | |
| 178 | |
| 179 def __init__(self, jid, password): | |
| 180 xmlstream.ConnectAuthenticator.__init__(self, jid.host) | |
| 181 self.jid = jid | |
| 182 self.password = password | |
| 183 | |
| 184 def associateWithStream(self, xs): | |
| 185 xs.version = (0, 0) | |
| 186 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) | |
| 187 | |
| 188 inits = [ (xmlstream.TLSInitiatingInitializer, False), | |
| 189 (IQAuthInitializer, True), | |
| 190 ] | |
| 191 | |
| 192 for initClass, required in inits: | |
| 193 init = initClass(xs) | |
| 194 init.required = required | |
| 195 xs.initializers.append(init) | |
| 196 | |
| 197 # TODO: move registration into an Initializer? | |
| 198 | |
| 199 def registerAccount(self, username = None, password = None): | |
| 200 if username: | |
| 201 self.jid.user = username | |
| 202 if password: | |
| 203 self.password = password | |
| 204 | |
| 205 iq = IQ(self.xmlstream, "set") | |
| 206 iq.addElement(("jabber:iq:register", "query")) | |
| 207 iq.query.addElement("username", content = self.jid.user) | |
| 208 iq.query.addElement("password", content = self.password) | |
| 209 | |
| 210 iq.addCallback(self._registerResultEvent) | |
| 211 | |
| 212 iq.send() | |
| 213 | |
| 214 def _registerResultEvent(self, iq): | |
| 215 if iq["type"] == "result": | |
| 216 # Registration succeeded -- go ahead and auth | |
| 217 self.streamStarted() | |
| 218 else: | |
| 219 # Registration failed | |
| 220 self.xmlstream.dispatch(iq, self.REGISTER_FAILED_EVENT) | |
| 221 | |
| 222 | |
| 223 | |
| 224 class CheckVersionInitializer(object): | |
| 225 """ | |
| 226 Initializer that checks if the minimum common stream version number is 1.0. | |
| 227 """ | |
| 228 | |
| 229 def __init__(self, xs): | |
| 230 self.xmlstream = xs | |
| 231 | |
| 232 | |
| 233 def initialize(self): | |
| 234 if self.xmlstream.version < (1, 0): | |
| 235 raise error.StreamError('unsupported-version') | |
| 236 | |
| 237 | |
| 238 | |
| 239 class BindInitializer(xmlstream.BaseFeatureInitiatingInitializer): | |
| 240 """ | |
| 241 Initializer that implements Resource Binding for the initiating entity. | |
| 242 | |
| 243 This protocol is documented in U{RFC 3920, section | |
| 244 7<http://www.xmpp.org/specs/rfc3920.html#bind>}. | |
| 245 """ | |
| 246 | |
| 247 feature = (NS_XMPP_BIND, 'bind') | |
| 248 | |
| 249 def start(self): | |
| 250 iq = xmlstream.IQ(self.xmlstream, 'set') | |
| 251 bind = iq.addElement((NS_XMPP_BIND, 'bind')) | |
| 252 resource = self.xmlstream.authenticator.jid.resource | |
| 253 if resource: | |
| 254 bind.addElement('resource', content=resource) | |
| 255 d = iq.send() | |
| 256 d.addCallback(self.onBind) | |
| 257 return d | |
| 258 | |
| 259 | |
| 260 def onBind(self, iq): | |
| 261 if iq.bind: | |
| 262 self.xmlstream.authenticator.jid = JID(unicode(iq.bind.jid)) | |
| 263 | |
| 264 | |
| 265 | |
| 266 class SessionInitializer(xmlstream.BaseFeatureInitiatingInitializer): | |
| 267 """ | |
| 268 Initializer that implements session establishment for the initiating | |
| 269 entity. | |
| 270 | |
| 271 This protocol is defined in U{RFC 3921, section | |
| 272 3<http://www.xmpp.org/specs/rfc3921.html#session>}. | |
| 273 """ | |
| 274 | |
| 275 feature = (NS_XMPP_SESSION, 'session') | |
| 276 | |
| 277 def start(self): | |
| 278 iq = xmlstream.IQ(self.xmlstream, 'set') | |
| 279 session = iq.addElement((NS_XMPP_SESSION, 'session')) | |
| 280 return iq.send() | |
| 281 | |
| 282 | |
| 283 | |
| 284 def XMPPClientFactory(jid, password): | |
| 285 """ | |
| 286 Client factory for XMPP 1.0 (only). | |
| 287 | |
| 288 This returns a L{xmlstream.XmlStreamFactory} with an L{XMPPAuthenticator} | |
| 289 object to perform the stream initialization steps (such as authentication). | |
| 290 | |
| 291 @see: The notes at L{XMPPAuthenticator} describe how the L{jid} and | |
| 292 L{password} parameters are to be used. | |
| 293 | |
| 294 @param jid: Jabber ID to connect with. | |
| 295 @type jid: L{jid.JID} | |
| 296 @param password: password to authenticate with. | |
| 297 @type password: L{unicode} | |
| 298 @return: XML stream factory. | |
| 299 @rtype: L{xmlstream.XmlStreamFactory} | |
| 300 """ | |
| 301 a = XMPPAuthenticator(jid, password) | |
| 302 return xmlstream.XmlStreamFactory(a) | |
| 303 | |
| 304 | |
| 305 | |
| 306 class XMPPAuthenticator(xmlstream.ConnectAuthenticator): | |
| 307 """ | |
| 308 Initializes an XmlStream connecting to an XMPP server as a Client. | |
| 309 | |
| 310 This authenticator performs the initialization steps needed to start | |
| 311 exchanging XML stanzas with an XMPP server as an XMPP client. It checks if | |
| 312 the server advertises XML stream version 1.0, negotiates TLS (when | |
| 313 available), performs SASL authentication, binds a resource and establishes | |
| 314 a session. | |
| 315 | |
| 316 Upon successful stream initialization, the L{xmlstream.STREAM_AUTHD_EVENT} | |
| 317 event will be dispatched through the XML stream object. Otherwise, the | |
| 318 L{xmlstream.INIT_FAILED_EVENT} event will be dispatched with a failure | |
| 319 object. | |
| 320 | |
| 321 After inspection of the failure, initialization can then be restarted by | |
| 322 calling L{initializeStream}. For example, in case of authentication | |
| 323 failure, a user may be given the opportunity to input the correct password. | |
| 324 By setting the L{password} instance variable and restarting initialization, | |
| 325 the stream authentication step is then retried, and subsequent steps are | |
| 326 performed if succesful. | |
| 327 | |
| 328 @ivar jid: Jabber ID to authenticate with. This may contain a resource | |
| 329 part, as a suggestion to the server for resource binding. A | |
| 330 server may override this, though. If the resource part is left | |
| 331 off, the server will generate a unique resource identifier. | |
| 332 The server will always return the full Jabber ID in the | |
| 333 resource binding step, and this is stored in this instance | |
| 334 variable. | |
| 335 @type jid: L{jid.JID} | |
| 336 @ivar password: password to be used during SASL authentication. | |
| 337 @type password: L{unicode} | |
| 338 """ | |
| 339 | |
| 340 namespace = 'jabber:client' | |
| 341 | |
| 342 def __init__(self, jid, password): | |
| 343 xmlstream.ConnectAuthenticator.__init__(self, jid.host) | |
| 344 self.jid = jid | |
| 345 self.password = password | |
| 346 | |
| 347 | |
| 348 def associateWithStream(self, xs): | |
| 349 """ | |
| 350 Register with the XML stream. | |
| 351 | |
| 352 Populates stream's list of initializers, along with their | |
| 353 requiredness. This list is used by | |
| 354 L{ConnectAuthenticator.initializeStream} to perform the initalization | |
| 355 steps. | |
| 356 """ | |
| 357 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) | |
| 358 | |
| 359 xs.initializers = [CheckVersionInitializer(xs)] | |
| 360 inits = [ (xmlstream.TLSInitiatingInitializer, False), | |
| 361 (sasl.SASLInitiatingInitializer, True), | |
| 362 (BindInitializer, False), | |
| 363 (SessionInitializer, False), | |
| 364 ] | |
| 365 | |
| 366 for initClass, required in inits: | |
| 367 init = initClass(xs) | |
| 368 init.required = required | |
| 369 xs.initializers.append(init) | |
| OLD | NEW |