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