client.py 14 KB

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