clientxmpp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. # -*- coding: utf-8 -*-
  2. """
  3. sleekxmpp.clientxmpp
  4. ~~~~~~~~~~~~~~~~~~~~
  5. This module provides XMPP functionality that
  6. is specific to client connections.
  7. Part of SleekXMPP: The Sleek XMPP Library
  8. :copyright: (c) 2011 Nathanael C. Fritz
  9. :license: MIT, see LICENSE for more details
  10. """
  11. from __future__ import absolute_import, unicode_literals
  12. import logging
  13. from sleekxmpp.stanza import StreamFeatures
  14. from sleekxmpp.basexmpp import BaseXMPP
  15. from sleekxmpp.exceptions import XMPPError
  16. from sleekxmpp.xmlstream import XMLStream
  17. from sleekxmpp.xmlstream.matcher import StanzaPath, MatchXPath
  18. from sleekxmpp.xmlstream.handler import Callback
  19. # Flag indicating if DNS SRV records are available for use.
  20. try:
  21. import dns.resolver
  22. except ImportError:
  23. DNSPYTHON = False
  24. else:
  25. DNSPYTHON = True
  26. log = logging.getLogger(__name__)
  27. class ClientXMPP(BaseXMPP):
  28. """
  29. SleekXMPP's client class. (Use only for good, not for evil.)
  30. Typical use pattern:
  31. .. code-block:: python
  32. xmpp = ClientXMPP('user@server.tld/resource', 'password')
  33. # ... Register plugins and event handlers ...
  34. xmpp.connect()
  35. xmpp.process(block=False) # block=True will block the current
  36. # thread. By default, block=False
  37. :param jid: The JID of the XMPP user account.
  38. :param password: The password for the XMPP user account.
  39. :param plugin_config: A dictionary of plugin configurations.
  40. :param plugin_whitelist: A list of approved plugins that
  41. will be loaded when calling
  42. :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
  43. :param escape_quotes: **Deprecated.**
  44. """
  45. def __init__(self, jid, password, plugin_config=None,
  46. plugin_whitelist=None, escape_quotes=True, sasl_mech=None,
  47. lang='en', **kwargs):
  48. if not plugin_whitelist:
  49. plugin_whitelist = []
  50. if not plugin_config:
  51. plugin_config = {}
  52. BaseXMPP.__init__(self, jid, 'jabber:client', **kwargs)
  53. self.escape_quotes = escape_quotes
  54. self.plugin_config = plugin_config
  55. self.plugin_whitelist = plugin_whitelist
  56. self.default_port = 5222
  57. self.default_lang = lang
  58. self.credentials = {}
  59. self.password = password
  60. self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % (
  61. self.boundjid.host,
  62. "xmlns:stream='%s'" % self.stream_ns,
  63. "xmlns='%s'" % self.default_ns,
  64. "xml:lang='%s'" % self.default_lang,
  65. "version='1.0'")
  66. self.stream_footer = "</stream:stream>"
  67. self.features = set()
  68. self._stream_feature_handlers = {}
  69. self._stream_feature_order = []
  70. self.dns_service = 'xmpp-client'
  71. #TODO: Use stream state here
  72. self.authenticated = False
  73. self.sessionstarted = False
  74. self.bound = False
  75. self.bindfail = False
  76. self.add_event_handler('connected', self._reset_connection_state)
  77. self.add_event_handler('session_bind', self._handle_session_bind)
  78. self.add_event_handler('roster_update', self._handle_roster)
  79. self.register_stanza(StreamFeatures)
  80. self.register_handler(
  81. Callback('Stream Features',
  82. MatchXPath('{%s}features' % self.stream_ns),
  83. self._handle_stream_features))
  84. self.register_handler(
  85. Callback('Roster Update',
  86. StanzaPath('iq@type=set/roster'),
  87. lambda iq: self.event('roster_update', iq)))
  88. # Setup default stream features
  89. self.register_plugin('feature_starttls')
  90. self.register_plugin('feature_bind')
  91. self.register_plugin('feature_session')
  92. self.register_plugin('feature_rosterver')
  93. self.register_plugin('feature_preapproval')
  94. self.register_plugin('feature_mechanisms')
  95. if sasl_mech:
  96. self['feature_mechanisms'].use_mech = sasl_mech
  97. @property
  98. def password(self):
  99. return self.credentials.get('password', '')
  100. @password.setter
  101. def password(self, value):
  102. self.credentials['password'] = value
  103. def connect(self, address=tuple(), reattempt=True,
  104. use_tls=True, use_ssl=False):
  105. """Connect to the XMPP server.
  106. When no address is given, a SRV lookup for the server will
  107. be attempted. If that fails, the server user in the JID
  108. will be used.
  109. :param address: A tuple containing the server's host and port.
  110. :param reattempt: If ``True``, repeat attempting to connect if an
  111. error occurs. Defaults to ``True``.
  112. :param use_tls: Indicates if TLS should be used for the
  113. connection. Defaults to ``True``.
  114. :param use_ssl: Indicates if the older SSL connection method
  115. should be used. Defaults to ``False``.
  116. """
  117. self.session_started_event.clear()
  118. # If an address was provided, disable using DNS SRV lookup;
  119. # otherwise, use the domain from the client JID with the standard
  120. # XMPP client port and allow SRV lookup.
  121. if address:
  122. self.dns_service = None
  123. else:
  124. address = (self.boundjid.host, 5222)
  125. self.dns_service = 'xmpp-client'
  126. return XMLStream.connect(self, address[0], address[1],
  127. use_tls=use_tls, use_ssl=use_ssl,
  128. reattempt=reattempt)
  129. def register_feature(self, name, handler, restart=False, order=5000):
  130. """Register a stream feature handler.
  131. :param name: The name of the stream feature.
  132. :param handler: The function to execute if the feature is received.
  133. :param restart: Indicates if feature processing should halt with
  134. this feature. Defaults to ``False``.
  135. :param order: The relative ordering in which the feature should
  136. be negotiated. Lower values will be attempted
  137. earlier when available.
  138. """
  139. self._stream_feature_handlers[name] = (handler, restart)
  140. self._stream_feature_order.append((order, name))
  141. self._stream_feature_order.sort()
  142. def unregister_feature(self, name, order):
  143. if name in self._stream_feature_handlers:
  144. del self._stream_feature_handlers[name]
  145. self._stream_feature_order.remove((order, name))
  146. self._stream_feature_order.sort()
  147. def update_roster(self, jid, **kwargs):
  148. """Add or change a roster item.
  149. :param jid: The JID of the entry to modify.
  150. :param name: The user's nickname for this JID.
  151. :param subscription: The subscription status. May be one of
  152. ``'to'``, ``'from'``, ``'both'``, or
  153. ``'none'``. If set to ``'remove'``,
  154. the entry will be deleted.
  155. :param groups: The roster groups that contain this item.
  156. :param block: Specify if the roster request will block
  157. until a response is received, or a timeout
  158. occurs. Defaults to ``True``.
  159. :param timeout: The length of time (in seconds) to wait
  160. for a response before continuing if blocking
  161. is used. Defaults to
  162. :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
  163. :param callback: Optional reference to a stream handler function.
  164. Will be executed when the roster is received.
  165. Implies ``block=False``.
  166. """
  167. current = self.client_roster[jid]
  168. name = kwargs.get('name', current['name'])
  169. subscription = kwargs.get('subscription', current['subscription'])
  170. groups = kwargs.get('groups', current['groups'])
  171. block = kwargs.get('block', True)
  172. timeout = kwargs.get('timeout', None)
  173. callback = kwargs.get('callback', None)
  174. return self.client_roster.update(jid, name, subscription, groups,
  175. block, timeout, callback)
  176. def del_roster_item(self, jid):
  177. """Remove an item from the roster.
  178. This is done by setting its subscription status to ``'remove'``.
  179. :param jid: The JID of the item to remove.
  180. """
  181. return self.client_roster.remove(jid)
  182. def get_roster(self, block=True, timeout=None, callback=None):
  183. """Request the roster from the server.
  184. :param block: Specify if the roster request will block until a
  185. response is received, or a timeout occurs.
  186. Defaults to ``True``.
  187. :param timeout: The length of time (in seconds) to wait for a response
  188. before continuing if blocking is used.
  189. Defaults to
  190. :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
  191. :param callback: Optional reference to a stream handler function. Will
  192. be executed when the roster is received.
  193. Implies ``block=False``.
  194. """
  195. iq = self.Iq()
  196. iq['type'] = 'get'
  197. iq.enable('roster')
  198. if 'rosterver' in self.features:
  199. iq['roster']['ver'] = self.client_roster.version
  200. if not block or callback is not None:
  201. block = False
  202. if callback is None:
  203. callback = lambda resp: self.event('roster_update', resp)
  204. else:
  205. orig_cb = callback
  206. def wrapped(resp):
  207. self.event('roster_update', resp)
  208. orig_cb(resp)
  209. callback = wrapped
  210. response = iq.send(block, timeout, callback)
  211. if block:
  212. self.event('roster_update', response)
  213. return response
  214. def _reset_connection_state(self, event=None):
  215. #TODO: Use stream state here
  216. self.authenticated = False
  217. self.sessionstarted = False
  218. self.bound = False
  219. self.bindfail = False
  220. self.features = set()
  221. def _handle_stream_features(self, features):
  222. """Process the received stream features.
  223. :param features: The features stanza.
  224. """
  225. for order, name in self._stream_feature_order:
  226. if name in features['features']:
  227. handler, restart = self._stream_feature_handlers[name]
  228. if handler(features) and restart:
  229. # Don't continue if the feature requires
  230. # restarting the XML stream.
  231. return True
  232. log.debug('Finished processing stream features.')
  233. self.event('stream_negotiated')
  234. def _handle_roster(self, iq):
  235. """Update the roster after receiving a roster stanza.
  236. :param iq: The roster stanza.
  237. """
  238. if iq['type'] == 'set':
  239. if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
  240. raise XMPPError(condition='service-unavailable')
  241. roster = self.client_roster
  242. if iq['roster']['ver']:
  243. roster.version = iq['roster']['ver']
  244. items = iq['roster']['items']
  245. valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
  246. for jid, item in items.items():
  247. if item['subscription'] in valid_subscriptions:
  248. roster[jid]['name'] = item['name']
  249. roster[jid]['groups'] = item['groups']
  250. roster[jid]['from'] = item['subscription'] in ('from', 'both')
  251. roster[jid]['to'] = item['subscription'] in ('to', 'both')
  252. roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
  253. roster[jid].save(remove=(item['subscription'] == 'remove'))
  254. if iq['type'] == 'set':
  255. resp = self.Iq(stype='result',
  256. sto=iq['from'],
  257. sid=iq['id'])
  258. resp.enable('roster')
  259. resp.send()
  260. def _handle_session_bind(self, jid):
  261. """Set the client roster to the JID set by the server.
  262. :param :class:`sleekxmpp.xmlstream.jid.JID` jid: The bound JID as
  263. dictated by the server. The same as :attr:`boundjid`.
  264. """
  265. self.client_roster = self.roster[jid]
  266. # To comply with PEP8, method names now use underscores.
  267. # Deprecated method names are re-mapped for backwards compatibility.
  268. ClientXMPP.updateRoster = ClientXMPP.update_roster
  269. ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
  270. ClientXMPP.getRoster = ClientXMPP.get_roster
  271. ClientXMPP.registerFeature = ClientXMPP.register_feature