Fix the iq.send() function, and a bunch of places where it is called
[slixmpp.git] / slixmpp / plugins / xep_0115 / caps.py
blob5974699ccfaffd929e95fddbe0cc7957fea44fb4
1 """
2 Slixmpp: The Slick XMPP Library
3 Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
4 This file is part of Slixmpp.
6 See the file LICENSE for copying permission.
7 """
9 import logging
10 import hashlib
11 import base64
13 from slixmpp import __version__
14 from slixmpp.stanza import StreamFeatures, Presence, Iq
15 from slixmpp.xmlstream import register_stanza_plugin, JID
16 from slixmpp.xmlstream.handler import Callback
17 from slixmpp.xmlstream.matcher import StanzaPath
18 from slixmpp.exceptions import XMPPError, IqError, IqTimeout
19 from slixmpp.plugins import BasePlugin
20 from slixmpp.plugins.xep_0115 import stanza, StaticCaps
23 log = logging.getLogger(__name__)
26 class XEP_0115(BasePlugin):
28 """
29 XEP-0115: Entity Capabalities
30 """
32 name = 'xep_0115'
33 description = 'XEP-0115: Entity Capabilities'
34 dependencies = set(['xep_0030', 'xep_0128', 'xep_0004'])
35 stanza = stanza
36 default_config = {
37 'hash': 'sha-1',
38 'caps_node': None,
39 'broadcast': True
42 def plugin_init(self):
43 self.hashes = {'sha-1': hashlib.sha1,
44 'sha1': hashlib.sha1,
45 'md5': hashlib.md5}
47 if self.caps_node is None:
48 self.caps_node = 'http://slixmpp.com/ver/%s' % __version__
50 register_stanza_plugin(Presence, stanza.Capabilities)
51 register_stanza_plugin(StreamFeatures, stanza.Capabilities)
53 self._disco_ops = ['cache_caps',
54 'get_caps',
55 'assign_verstring',
56 'get_verstring',
57 'supports',
58 'has_identity']
60 self.xmpp.register_handler(
61 Callback('Entity Capabilites',
62 StanzaPath('presence/caps'),
63 self._handle_caps))
65 self.xmpp.add_filter('out', self._filter_add_caps)
67 self.xmpp.add_event_handler('entity_caps', self._process_caps)
69 if not self.xmpp.is_component:
70 self.xmpp.register_feature('caps',
71 self._handle_caps_feature,
72 restart=False,
73 order=10010)
75 disco = self.xmpp['xep_0030']
76 self.static = StaticCaps(self.xmpp, disco.static)
78 for op in self._disco_ops:
79 self.api.register(getattr(self.static, op), op, default=True)
81 for op in ('supports', 'has_identity'):
82 self.xmpp['xep_0030'].api.register(getattr(self.static, op), op)
84 self._run_node_handler = disco._run_node_handler
86 disco.cache_caps = self.cache_caps
87 disco.update_caps = self.update_caps
88 disco.assign_verstring = self.assign_verstring
89 disco.get_verstring = self.get_verstring
91 def plugin_end(self):
92 self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace)
93 self.xmpp.del_filter('out', self._filter_add_caps)
94 self.xmpp.del_event_handler('entity_caps', self._process_caps)
95 self.xmpp.remove_handler('Entity Capabilities')
96 if not self.xmpp.is_component:
97 self.xmpp.unregister_feature('caps', 10010)
98 for op in ('supports', 'has_identity'):
99 self.xmpp['xep_0030'].restore_defaults(op)
101 def session_bind(self, jid):
102 self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
104 def _filter_add_caps(self, stanza):
105 if not isinstance(stanza, Presence) or not self.broadcast:
106 return stanza
108 if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'):
109 return stanza
111 ver = self.get_verstring(stanza['from'])
112 if ver:
113 stanza['caps']['node'] = self.caps_node
114 stanza['caps']['hash'] = self.hash
115 stanza['caps']['ver'] = ver
116 return stanza
118 def _handle_caps(self, presence):
119 if not self.xmpp.is_component:
120 if presence['from'] == self.xmpp.boundjid:
121 return
122 self.xmpp.event('entity_caps', presence)
124 def _handle_caps_feature(self, features):
125 # We already have a method to process presence with
126 # caps, so wrap things up and use that.
127 p = Presence()
128 p['from'] = self.xmpp.boundjid.domain
129 p.append(features['caps'])
130 self.xmpp.features.add('caps')
132 self.xmpp.event('entity_caps', p)
134 def _process_caps(self, pres):
135 if not pres['caps']['hash']:
136 log.debug("Received unsupported legacy caps: %s, %s, %s",
137 pres['caps']['node'],
138 pres['caps']['ver'],
139 pres['caps']['ext'])
140 self.xmpp.event('entity_caps_legacy', pres)
141 return
143 ver = pres['caps']['ver']
145 existing_verstring = self.get_verstring(pres['from'].full)
146 if str(existing_verstring) == str(ver):
147 return
149 existing_caps = self.get_caps(verstring=ver)
150 if existing_caps is not None:
151 self.assign_verstring(pres['from'], ver)
152 return
154 if pres['caps']['hash'] not in self.hashes:
155 try:
156 log.debug("Unknown caps hash: %s", pres['caps']['hash'])
157 self.xmpp['xep_0030'].get_info(jid=pres['from'])
158 return
159 except XMPPError:
160 return
162 log.debug("New caps verification string: %s", ver)
163 try:
164 node = '%s#%s' % (pres['caps']['node'], ver)
165 caps = self.xmpp['xep_0030'].get_info(pres['from'], node)
167 if isinstance(caps, Iq):
168 caps = caps['disco_info']
170 if self._validate_caps(caps, pres['caps']['hash'],
171 pres['caps']['ver']):
172 self.assign_verstring(pres['from'], pres['caps']['ver'])
173 except XMPPError:
174 log.debug("Could not retrieve disco#info results for caps for %s", node)
176 def _validate_caps(self, caps, hash, check_verstring):
177 # Check Identities
178 full_ids = caps.get_identities(dedupe=False)
179 deduped_ids = caps.get_identities()
180 if len(full_ids) != len(deduped_ids):
181 log.debug("Duplicate disco identities found, invalid for caps")
182 return False
184 # Check Features
185 full_features = caps.get_features(dedupe=False)
186 deduped_features = caps.get_features()
187 if len(full_features) != len(deduped_features):
188 log.debug("Duplicate disco features found, invalid for caps")
189 return False
191 # Check Forms
192 form_types = []
193 deduped_form_types = set()
194 for stanza in caps['substanzas']:
195 if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
196 log.debug("Non form extension found, ignoring for caps")
197 caps.xml.remove(stanza.xml)
198 continue
199 if 'FORM_TYPE' in stanza['fields']:
200 f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
201 form_types.append(f_type)
202 deduped_form_types.add(f_type)
203 if len(form_types) != len(deduped_form_types):
204 log.debug("Duplicated FORM_TYPE values, " + \
205 "invalid for caps")
206 return False
208 if len(f_type) > 1:
209 deduped_type = set(f_type)
210 if len(f_type) != len(deduped_type):
211 log.debug("Extra FORM_TYPE data, invalid for caps")
212 return False
214 if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
215 log.debug("Field FORM_TYPE type not 'hidden', " + \
216 "ignoring form for caps")
217 caps.xml.remove(stanza.xml)
218 else:
219 log.debug("No FORM_TYPE found, ignoring form for caps")
220 caps.xml.remove(stanza.xml)
222 verstring = self.generate_verstring(caps, hash)
223 if verstring != check_verstring:
224 log.debug("Verification strings do not match: %s, %s" % (
225 verstring, check_verstring))
226 return False
228 self.cache_caps(verstring, caps)
229 return True
231 def generate_verstring(self, info, hash):
232 hash = self.hashes.get(hash, None)
233 if hash is None:
234 return None
236 S = ''
238 # Convert None to '' in the identities
239 def clean_identity(id):
240 return map(lambda i: i or '', id)
241 identities = map(clean_identity, info['identities'])
243 identities = sorted(('/'.join(i) for i in identities))
244 features = sorted(info['features'])
246 S += '<'.join(identities) + '<'
247 S += '<'.join(features) + '<'
249 form_types = {}
251 for stanza in info['substanzas']:
252 if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
253 if 'FORM_TYPE' in stanza['fields']:
254 f_type = stanza['values']['FORM_TYPE']
255 if len(f_type):
256 f_type = f_type[0]
257 if f_type not in form_types:
258 form_types[f_type] = []
259 form_types[f_type].append(stanza)
261 sorted_forms = sorted(form_types.keys())
262 for f_type in sorted_forms:
263 for form in form_types[f_type]:
264 S += '%s<' % f_type
265 fields = sorted(form['fields'].keys())
266 fields.remove('FORM_TYPE')
267 for field in fields:
268 S += '%s<' % field
269 vals = form['fields'][field].get_value(convert=False)
270 if vals is None:
271 S += '<'
272 else:
273 if not isinstance(vals, list):
274 vals = [vals]
275 S += '<'.join(sorted(vals)) + '<'
277 binary = hash(S.encode('utf8')).digest()
278 return base64.b64encode(binary).decode('utf-8')
280 def update_caps(self, jid=None, node=None, preserve=False):
281 try:
282 info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
283 if isinstance(info, Iq):
284 info = info['disco_info']
285 ver = self.generate_verstring(info, self.hash)
286 self.xmpp['xep_0030'].set_info(
287 jid=jid,
288 node='%s#%s' % (self.caps_node, ver),
289 info=info)
290 self.cache_caps(ver, info)
291 self.assign_verstring(jid, ver)
293 if self.xmpp.sessionstarted and self.broadcast:
294 if self.xmpp.is_component or preserve:
295 for contact in self.xmpp.roster[jid]:
296 self.xmpp.roster[jid][contact].send_last_presence()
297 else:
298 self.xmpp.roster[jid].send_last_presence()
299 except XMPPError:
300 return
302 def get_verstring(self, jid=None):
303 if jid in ('', None):
304 jid = self.xmpp.boundjid.full
305 if isinstance(jid, JID):
306 jid = jid.full
307 return self.api['get_verstring'](jid)
309 def assign_verstring(self, jid=None, verstring=None):
310 if jid in (None, ''):
311 jid = self.xmpp.boundjid.full
312 if isinstance(jid, JID):
313 jid = jid.full
314 return self.api['assign_verstring'](jid, args={
315 'verstring': verstring})
317 def cache_caps(self, verstring=None, info=None):
318 data = {'verstring': verstring, 'info': info}
319 return self.api['cache_caps'](args=data)
321 def get_caps(self, jid=None, verstring=None):
322 if verstring is None:
323 if jid is not None:
324 verstring = self.get_verstring(jid)
325 else:
326 return None
327 if isinstance(jid, JID):
328 jid = jid.full
329 data = {'verstring': verstring}
330 return self.api['get_caps'](jid, args=data)