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