client.py 14 KB

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