From 8a29ec67ac91528747c2a954cc8f89b5bbe71e58 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 21:45:25 -0500 Subject: Add XEP-0115 plugin. Finally --- sleekxmpp/plugins/xep_0115/caps.py | 260 +++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0115/caps.py (limited to 'sleekxmpp/plugins/xep_0115/caps.py') diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py new file mode 100644 index 00000000..19570d18 --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -0,0 +1,260 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import hashlib +import base64 + +import sleekxmpp +from sleekxmpp import Presence, Iq +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps + + +log = logging.getLogger(__name__) + + +class xep_0115(base_plugin): + + """ + XEP-0115: Entity Capabalities + """ + + def plugin_init(self): + self.xep = '0115' + self.description = 'Entity Capabilities' + self.stanza = stanza + + self.hashes = {'sha-1': hashlib.sha1, + 'md5': hashlib.md5} + + self.hash = self.config.get('hash', 'sha-1') + self.caps_node = self.config.get('caps_node', None) + self.broadcast = self.config.get('broadcast', True) + + if self.caps_node is None: + ver = sleekxmpp.__version__ + self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver + + register_stanza_plugin(Presence, stanza.Capabilities) + + self._disco_ops = ['cache_caps', + 'get_caps', + 'assign_verstring', + 'get_verstring', + 'supports', + 'has_identity'] + + self.xmpp.register_handler( + Callback('Entity Capabilites', + StanzaPath('presence/caps'), + self._handle_caps)) + + self.xmpp.add_filter('out', self._filter_add_caps) + + self.xmpp.add_event_handler('entity_caps', self._process_caps, + threaded=True) + + def post_init(self): + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) + + self.disco = self.xmpp['xep_0030'] + self.static = StaticCaps(self.xmpp, self.disco.static) + + for op in self._disco_ops: + self.disco._add_disco_op(op, getattr(self.static, op)) + + self._run_node_handler = self.disco._run_node_handler + + self.disco.cache_caps = self.cache_caps + self.disco.update_caps = self.update_caps + self.disco.assign_verstring = self.assign_verstring + self.disco.get_verstring = self.get_verstring + + def _filter_add_caps(self, stanza): + if isinstance(stanza, Presence) and self.broadcast: + ver = self.get_verstring(stanza['from']) + if ver: + stanza['caps']['node'] = self.caps_node + stanza['caps']['hash'] = self.hash + stanza['caps']['ver'] = ver + return stanza + + def _handle_caps(self, presence): + if not self.xmpp.is_component: + if presence['from'] == self.xmpp.boundjid: + return + self.xmpp.event('entity_caps', presence) + + def _process_caps(self, pres): + if not pres['caps']['hash']: + log.debug("Received unsupported legacy caps.") + self.xmpp.event('entity_caps_legacy', pres) + return + + existing_verstring = self.get_verstring(pres['from'].full) + if str(existing_verstring) == str(pres['caps']['ver']): + return + + if pres['caps']['hash'] not in self.hashes: + try: + log.debug("Unknown caps hash: %s", pres['caps']['hash']) + self.disco.get_info(jid=pres['from']) + return + except XMPPError: + return + + log.debug("New caps verification string: %s", pres['caps']['ver']) + try: + caps = self.disco.get_info( + jid=pres['from'], + node='%s#%s' % (pres['caps']['node'], + pres['caps']['ver'])) + + if self._validate_caps(caps['disco_info'], + pres['caps']['hash'], + pres['caps']['ver']): + self.assign_verstring(pres['from'], pres['caps']['ver']) + except XMPPError: + log.debug("Could not retrieve disco#info results for caps") + + def _validate_caps(self, caps, hash, check_verstring): + # Check Identities + full_ids = caps.get_identities(dedupe=False) + deduped_ids = caps.get_identities() + if len(full_ids) != len(deduped_ids): + log.debug("Duplicate disco identities found, invalid for caps") + return False + + # Check Features + + full_features = caps.get_features(dedupe=False) + deduped_features = caps.get_features() + if len(full_features) != len(deduped_features): + log.debug("Duplicate disco features found, invalid for caps") + return False + + # Check Forms + form_types = [] + deduped_form_types = set() + for stanza in caps['substanzas']: + if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + if 'FORM_TYPE' in stanza['fields']: + f_type = tuple(stanza['fields']['FORM_TYPE']['value']) + form_types.append(f_type) + deduped_form_types.add(f_type) + if len(form_types) != len(deduped_form_types): + log.debug("Duplicated FORM_TYPE values, invalid for caps") + return False + + if len(f_type) > 1: + deduped_type = set(f_type) + if len(f_type) != len(deduped_type): + log.debug("Extra FORM_TYPE data, invalid for caps") + return False + + if stanza['fields']['FORM_TYPE']['type'] != 'hidden': + log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps") + caps.xml.remove(stanza.xml) + else: + log.debug("No FORM_TYPE found, ignoring form for caps") + caps.xml.remove(stanza.xml) + + verstring = self.generate_verstring(caps, hash) + if verstring != check_verstring: + log.debug("Verification strings do not match: %s, %s" % ( + verstring, check_verstring)) + return False + + self.cache_caps(verstring, caps) + return True + + def generate_verstring(self, info, hash): + hash = self.hashes.get(hash, None) + if hash is None: + return None + + S = '' + + # Convert None to '' in the identities + def clean_identity(id): + return map(lambda i: i or '', id) + identities = map(clean_identity, info['identities']) + + identities = sorted(('/'.join(i) for i in identities)) + features = sorted(info['features']) + + S += '<'.join(identities) + '<' + S += '<'.join(features) + '<' + + form_types = {} + + for stanza in info['substanzas']: + if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + if 'FORM_TYPE' in stanza['fields']: + f_type = stanza['values']['FORM_TYPE'] + if len(f_type): + f_type = f_type[0] + if f_type not in form_types: + form_types[f_type] = [] + form_types[f_type].append(stanza) + + sorted_forms = sorted(form_types.keys()) + for f_type in sorted_forms: + for form in form_types[f_type]: + S += '%s<' % f_type + fields = sorted(form['fields'].keys()) + fields.remove('FORM_TYPE') + for field in fields: + S += '%s<' % field + vals = form['fields'][field].get_value(convert=False) + if vals is None: + S += '<' + else: + if not isinstance(vals, list): + vals = [vals] + S += '<'.join(sorted(vals)) + '<' + + binary = hash(S.encode('utf8')).digest() + return base64.b64encode(binary) + + def update_caps(self, jid=None, node=None): + info = self.disco.get_info(jid, node, local=True) + if isinstance(info, Iq): + info = info['disco_info'] + ver = self.generate_verstring(info, self.hash) + self.cache_caps(ver, info) + self.assign_verstring(jid, ver) + + def get_verstring(self, jid=None): + return self._run_node_handler('get_verstring', jid) + + def assign_verstring(self, jid=None, verstring=None): + if jid in (None, ''): + jid = self.xmpp.boundjid.full + return self._run_node_handler('assign_verstring', jid, + data={'verstring': verstring}) + + def cache_caps(self, verstring=None, info=None): + data = {'verstring': verstring, 'info': info} + return self._run_node_handler('cache_caps', None, None, data=data) + + def get_caps(self, jid=None, verstring=None): + if verstring is None: + if jid is not None: + verstring = self.get_verstring(jid) + else: + return None + + data = {'verstring': verstring} + return self._run_node_handler('get_caps', jid, None, None, data) -- cgit v1.2.3 From d817d64c65c62976481690c0bfc3882621ac0455 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 22:34:57 -0500 Subject: Enable caps stream feature. --- sleekxmpp/plugins/xep_0115/caps.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'sleekxmpp/plugins/xep_0115/caps.py') diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 19570d18..306629f6 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -11,7 +11,7 @@ import hashlib import base64 import sleekxmpp -from sleekxmpp import Presence, Iq +from sleekxmpp.stanza import StreamFeatures, Presence, Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath @@ -46,6 +46,7 @@ class xep_0115(base_plugin): self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver register_stanza_plugin(Presence, stanza.Capabilities) + register_stanza_plugin(StreamFeatures, stanza.Capabilities) self._disco_ops = ['cache_caps', 'get_caps', @@ -64,6 +65,11 @@ class xep_0115(base_plugin): self.xmpp.add_event_handler('entity_caps', self._process_caps, threaded=True) + self.xmpp.register_feature('caps', + self._handle_caps_feature, + restart=False, + order=10010) + def post_init(self): base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) @@ -96,6 +102,16 @@ class xep_0115(base_plugin): return self.xmpp.event('entity_caps', presence) + def _handle_caps_feature(self, features): + # We already have a method to process presence with + # caps, so wrap things up and use that. + p = Presence() + p['from'] = self.xmpp.boundjid.domain + p.append(features['caps']) + self.xmpp.features.add('caps') + + self.xmpp.event('entity_caps', p) + def _process_caps(self, pres): if not pres['caps']['hash']: log.debug("Received unsupported legacy caps.") -- cgit v1.2.3 From 35954cdc90f1f404c81b7ede47c2fae420e182e8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 31 Dec 2011 19:18:00 -0500 Subject: Fix a few holes in caps. Protip: Don't test using a custom disco handler that always returns the same feature set :p --- sleekxmpp/plugins/xep_0115/caps.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'sleekxmpp/plugins/xep_0115/caps.py') diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 306629f6..db5b9bf3 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -245,14 +245,23 @@ class xep_0115(base_plugin): return base64.b64encode(binary) def update_caps(self, jid=None, node=None): - info = self.disco.get_info(jid, node, local=True) - if isinstance(info, Iq): - info = info['disco_info'] - ver = self.generate_verstring(info, self.hash) - self.cache_caps(ver, info) - self.assign_verstring(jid, ver) + try: + info = self.disco.get_info(jid, node, local=True) + if isinstance(info, Iq): + info = info['disco_info'] + ver = self.generate_verstring(info, self.hash) + self.disco.set_info( + jid=jid, + node='%s#%s' % (self.caps_node, ver), + info=info) + self.cache_caps(ver, info) + self.assign_verstring(jid, ver) + except XMPPError: + return def get_verstring(self, jid=None): + if jid in ('', None): + jid = self.xmpp.boundjid.full return self._run_node_handler('get_verstring', jid) def assign_verstring(self, jid=None, verstring=None): -- cgit v1.2.3 From 27c658922e51aaae722880e18878cb2964cb4bd0 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 31 Dec 2011 21:15:40 -0500 Subject: Fix handing caps in Python3, allow update_caps() call before process() --- sleekxmpp/plugins/xep_0115/caps.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) (limited to 'sleekxmpp/plugins/xep_0115/caps.py') diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index db5b9bf3..d3e62abb 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -12,7 +12,7 @@ import base64 import sleekxmpp from sleekxmpp.stanza import StreamFeatures, Presence, Iq -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout @@ -74,18 +74,18 @@ class xep_0115(base_plugin): base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) - self.disco = self.xmpp['xep_0030'] - self.static = StaticCaps(self.xmpp, self.disco.static) + disco = self.xmpp['xep_0030'] + self.static = StaticCaps(self.xmpp, disco.static) for op in self._disco_ops: - self.disco._add_disco_op(op, getattr(self.static, op)) + disco._add_disco_op(op, getattr(self.static, op)) - self._run_node_handler = self.disco._run_node_handler + self._run_node_handler = disco._run_node_handler - self.disco.cache_caps = self.cache_caps - self.disco.update_caps = self.update_caps - self.disco.assign_verstring = self.assign_verstring - self.disco.get_verstring = self.get_verstring + disco.cache_caps = self.cache_caps + disco.update_caps = self.update_caps + disco.assign_verstring = self.assign_verstring + disco.get_verstring = self.get_verstring def _filter_add_caps(self, stanza): if isinstance(stanza, Presence) and self.broadcast: @@ -125,15 +125,15 @@ class xep_0115(base_plugin): if pres['caps']['hash'] not in self.hashes: try: log.debug("Unknown caps hash: %s", pres['caps']['hash']) - self.disco.get_info(jid=pres['from']) + self.xmpp['xep_003'].get_info(jid=pres['from'].full) return except XMPPError: return log.debug("New caps verification string: %s", pres['caps']['ver']) try: - caps = self.disco.get_info( - jid=pres['from'], + caps = self.xmpp['xep_0030'].get_info( + jid=pres['from'].full, node='%s#%s' % (pres['caps']['node'], pres['caps']['ver'])) @@ -242,15 +242,15 @@ class xep_0115(base_plugin): S += '<'.join(sorted(vals)) + '<' binary = hash(S.encode('utf8')).digest() - return base64.b64encode(binary) + return base64.b64encode(binary).decode('utf-8') def update_caps(self, jid=None, node=None): try: - info = self.disco.get_info(jid, node, local=True) + info = self.xmpp['xep_0030'].get_info(jid, node, local=True) if isinstance(info, Iq): info = info['disco_info'] ver = self.generate_verstring(info, self.hash) - self.disco.set_info( + self.xmpp['xep_0030'].set_info( jid=jid, node='%s#%s' % (self.caps_node, ver), info=info) @@ -262,11 +262,15 @@ class xep_0115(base_plugin): def get_verstring(self, jid=None): if jid in ('', None): jid = self.xmpp.boundjid.full + if isinstance(jid, JID): + jid = jid.full return self._run_node_handler('get_verstring', jid) def assign_verstring(self, jid=None, verstring=None): if jid in (None, ''): jid = self.xmpp.boundjid.full + if isinstance(jid, JID): + jid = jid.full return self._run_node_handler('assign_verstring', jid, data={'verstring': verstring}) @@ -280,6 +284,7 @@ class xep_0115(base_plugin): verstring = self.get_verstring(jid) else: return None - + if isinstance(jid, JID): + jid = jid.full data = {'verstring': verstring} return self._run_node_handler('get_caps', jid, None, None, data) -- cgit v1.2.3