1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 """
27 Module containing all XEP-115 (Entity Capabilities) related classes
28
29 Basic Idea:
30 CapsCache caches features to hash relationships. The cache is queried
31 through ClientCaps objects which are hold by contact instances.
32 """
33
34 import base64
35 import hashlib
36
37 import logging
38 log = logging.getLogger('gajim.c.caps_cache')
39
40 from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES,
41 NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS)
42
43 FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION,
44 NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO]
45
46
47 NEW = 0
48 QUERIED = 1
49 CACHED = 2
50 FAKED = 3
51
52
53
54
55
56 capscache = None
63
65 lookup_item = client_caps.get_cache_lookup_strategy()
66 cache_item = lookup_item(capscache)
67
68 supported_features = cache_item.features
69 if requested_feature in supported_features:
70 return True
71 elif not supported_features and cache_item.status in (NEW, QUERIED, FAKED):
72
73
74 return requested_feature not in FEATURE_BLACKLIST
75 else:
76 return False
77
79 """
80 Create and return a suitable ClientCaps object for the given node,
81 caps_hash, hash_method combination.
82 """
83 if not node or not caps_hash:
84
85 client_caps = NullClientCaps()
86 elif not hash_method:
87 client_caps = OldClientCaps(caps_hash, node)
88 else:
89 client_caps = ClientCaps(caps_hash, node, hash_method)
90 return client_caps
91
93 """
94 Compute caps hash according to XEP-0115, V1.5
95
96 dataforms are xmpp.DataForms objects as common.dataforms don't allow several
97 values without a field type list-multi
98 """
99 def sort_identities_func(i1, i2):
100 cat1 = i1['category']
101 cat2 = i2['category']
102 if cat1 < cat2:
103 return -1
104 if cat1 > cat2:
105 return 1
106 type1 = i1.get('type', '')
107 type2 = i2.get('type', '')
108 if type1 < type2:
109 return -1
110 if type1 > type2:
111 return 1
112 lang1 = i1.get('xml:lang', '')
113 lang2 = i2.get('xml:lang', '')
114 if lang1 < lang2:
115 return -1
116 if lang1 > lang2:
117 return 1
118 return 0
119
120 def sort_dataforms_func(d1, d2):
121 f1 = d1.getField('FORM_TYPE')
122 f2 = d2.getField('FORM_TYPE')
123 if f1 and f2 and (f1.getValue() < f2.getValue()):
124 return -1
125 return 1
126
127 S = ''
128 identities.sort(cmp=sort_identities_func)
129 for i in identities:
130 c = i['category']
131 type_ = i.get('type', '')
132 lang = i.get('xml:lang', '')
133 name = i.get('name', '')
134 S += '%s/%s/%s/%s<' % (c, type_, lang, name)
135 features.sort()
136 for f in features:
137 S += '%s<' % f
138 dataforms.sort(cmp=sort_dataforms_func)
139 for dataform in dataforms:
140
141 fields = {}
142 for f in dataform.getChildren():
143 fields[f.getVar()] = f
144 form_type = fields.get('FORM_TYPE')
145 if form_type:
146 S += form_type.getValue() + '<'
147 del fields['FORM_TYPE']
148 for var in sorted(fields.keys()):
149 S += '%s<' % var
150 values = sorted(fields[var].getValues())
151 for value in values:
152 S += '%s<' % value
153
154 if hash_method == 'sha-1':
155 hash_ = hashlib.sha1(S)
156 elif hash_method == 'md5':
157 hash_ = hashlib.md5(S)
158 else:
159 return ''
160 return base64.b64encode(hash_.digest())
161
162
163
164
165
166
168 """
169 Base class representing a client and its capabilities as advertised by a
170 caps tag in a presence
171 """
173 self._hash = caps_hash
174 self._node = node
175
178
180 """
181 To be implemented by subclassess
182 """
183 raise NotImplementedError
184
187
189 """
190 To be implemented by subclassess
191 """
192 raise NotImplementedError
193
196
198 """
199 To be implemented by subclassess
200 """
201 raise NotImplementedError
202
203
205 """
206 The current XEP-115 implementation
207 """
208 - def __init__(self, caps_hash, node, hash_method):
212
214 return caps_cache[(self._hash_method, self._hash)]
215
218
223
224
226 """
227 Old XEP-115 implemtation. Kept around for background competability
228 """
231
233 return caps_cache[('old', self._node + '#' + self._hash)]
234
237
240
241
243 """
244 This is a NULL-Object to streamline caps handling if a client has not
245 advertised any caps or has advertised them in an improper way
246
247 Assumes (almost) everything is supported.
248 """
249 _instance = None
250 - def __new__(cls, *args, **kwargs):
258
261
268
271
274
275
277 """
278 This object keeps the mapping between caps data and real disco features they
279 represent, and provides simple way to query that info
280 """
282
283
284
285
286
287 self.__cache = {}
288
289 class CacheItem(object):
290
291
292
293 __names = {}
294
295 def __init__(self, hash_method, hash_, logger):
296
297 self.hash_method = hash_method
298 self.hash = hash_
299 self._features = []
300 self._identities = []
301 self._logger = logger
302
303 self.status = NEW
304 self._recently_seen = False
305
306 def _get_features(self):
307 return self._features
308
309 def _set_features(self, value):
310 self._features = []
311 for feature in value:
312 self._features.append(self.__names.setdefault(feature, feature))
313
314 features = property(_get_features, _set_features)
315
316 def _get_identities(self):
317 list_ = []
318 for i in self._identities:
319
320 d = dict()
321 d['category'] = i[0]
322 if i[1]:
323 d['type'] = i[1]
324 if i[2]:
325 d['xml:lang'] = i[2]
326 if i[3]:
327 d['name'] = i[3]
328 list_.append(d)
329 return list_
330
331 def _set_identities(self, value):
332 self._identities = []
333 for identity in value:
334
335 t = (identity['category'], identity.get('type'),
336 identity.get('xml:lang'), identity.get('name'))
337 self._identities.append(self.__names.setdefault(t, t))
338
339 identities = property(_get_identities, _set_identities)
340
341 def set_and_store(self, identities, features):
342 self.identities = identities
343 self.features = features
344 self._logger.add_caps_entry(self.hash_method, self.hash,
345 identities, features)
346 self.status = CACHED
347
348 def update_last_seen(self):
349 if not self._recently_seen:
350 self._recently_seen = True
351 self._logger.update_caps_time(self.hash_method, self.hash)
352
353 def is_valid(self):
354 """
355 Returns True if identities and features for this cache item
356 are known.
357 """
358 return self.status in (CACHED, FAKED)
359
360 self.__CacheItem = CacheItem
361 self.logger = logger
362
371
377
379 if caps in self.__cache:
380 return self.__cache[caps]
381
382 hash_method, hash_ = caps
383
384 x = self.__CacheItem(hash_method, hash_, self.logger)
385 self.__cache[(hash_method, hash_)] = x
386 return x
387
389 """
390 Start a disco query to determine caps (node, ver, exts). Won't query if
391 the data is already in cache
392 """
393 lookup_cache_item = client_caps.get_cache_lookup_strategy()
394 q = lookup_cache_item(self)
395
396 if q.status == NEW:
397
398
399 q.status = QUERIED
400 discover = client_caps.get_discover_strategy()
401 discover(connection, jid)
402 else:
403 q.update_last_seen()
404