core, plugin XEP-0115: discovery refactoring:

- hashing algorithm of XEP-0115 has been including in core
    - our own hash is still calculated by XEP-0115 and can be regenerated with XEP_0115.recalculateHash
    - old discovery methods have been removed. Now the following methods are used:
        - hasFeature: tell if a feature is available for an entity
        - getDiscoInfos: self explaining
        - getDiscoItems: self explaining
        - findServiceEntities: return all available items of an entity which given (category, type)
        - findFeaturesSet: search for a set of features in entity + entity's items
      all these methods are asynchronous, and manage cache automatically
    - XEP-0115 manage in a better way hashes, and now use a trigger for presence instead of monkey patch
    - new FeatureNotFound exception, when we want to do something which is not available
    - refactored client initialisation sequence, removed client.initialized Deferred
    - added constant APP_URL
    - test_plugin_xep_0033.py has been temporarly deactivated, the time to adapt it
    - lot of cleaning
This commit is contained in:
Goffi 2014-03-28 18:07:22 +01:00
commit 95678d7967

View file

@ -22,16 +22,10 @@ from sat.core.constants import Const as C
from logging import debug, info, error, warning
from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid
from sat.memory.persistent import PersistentBinaryDict
import types
from twisted.internet import defer
from zope.interface import implements
from wokkel import disco, iwokkel
from hashlib import sha1
from base64 import b64encode
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
@ -53,97 +47,44 @@ PLUGIN_INFO = {
}
class HashGenerationError(Exception):
pass
class ByteIdentity(object):
"""This class manage identity as bytes (needed for i;octet sort),
it is used for the hash generation"""
def __init__(self, identity, lang=None):
assert isinstance(identity, disco.DiscoIdentity)
self.category = identity.category.encode('utf-8')
self.idType = identity.type.encode('utf-8')
self.name = identity.name.encode('utf-8') if identity.name else ''
self.lang = lang.encode('utf-8') if lang else ''
def __str__(self):
return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name)
class XEP_0115(object):
cap_hash = None # capabilities hash is class variable as it is common to all profiles
#TODO: this code is really dirty, need to clean it and try to move it to Wokkel
def __init__(self, host):
info(_("Plugin XEP_0115 initialization"))
self.host = host
host.trigger.add("Disco Handled", self.checkHash)
self.hash_cache = PersistentBinaryDict(NS_ENTITY_CAPABILITY) # key = hash or jid, value = features
self.hash_cache.load()
self.jid_hash = {} # jid to hash mapping, map to a discoInfo features if the hash method is unknown
def checkHash(self, profile):
if not XEP_0115.cap_hash:
XEP_0115.cap_hash = self.generateHash(profile)
else:
self.presenceHack(profile)
return True
host.trigger.add("Disco handled", self._checkHash)
host.trigger.add("Presence send", self._presenceTrigger)
def getHandler(self, profile):
return XEP_0115_handler(self, profile)
def presenceHack(self, profile):
"""modify SatPresenceProtocol to add capabilities data"""
def _checkHash(self, disco_d, profile):
if XEP_0115.cap_hash is None:
disco_d.addCallback(lambda dummy: self.recalculateHash(profile))
return True
def _presenceTrigger(self, obj):
if XEP_0115.cap_hash is not None:
obj.addChild(XEP_0115.c_elt)
return True
@defer.inlineCallbacks
def recalculateHash(self, profile):
client = self.host.getClient(profile)
presenceInst = client.presence
disco_infos = yield client.discoHandler.info(client.jid, client.jid, '')
cap_hash = self.host.memory.disco.generateHash(disco_infos)
info("Our capability hash has been generated: [%s]" % cap_hash)
debug("Generating capability domish.Element")
c_elt = domish.Element((NS_ENTITY_CAPABILITY, 'c'))
c_elt['hash'] = 'sha-1'
c_elt['node'] = 'http://sat.goffi.org'
c_elt['ver'] = XEP_0115.cap_hash
presenceInst._c_elt = c_elt
if "_legacy_send" in dir(presenceInst):
debug('capabilities already added to presence instance')
return
def hacked_send(self, obj):
obj.addChild(self._c_elt)
self._legacy_send(obj)
new_send = types.MethodType(hacked_send, presenceInst, presenceInst.__class__)
presenceInst._legacy_send = presenceInst.send
presenceInst.send = new_send
def generateHash(self, profile_key=C.PROF_KEY_NONE):
"""This method generate a sha1 hash as explained in xep-0115 #5.1
it then store it in XEP_0115.cap_hash"""
profile = self.host.memory.getProfileName(profile_key)
if not profile:
error('Requesting hash for an inexistant profile')
raise HashGenerationError
client = self.host.getClient(profile_key)
def generateHash_2(services, profile):
_s = []
byte_identities = [ByteIdentity(service) for service in services if isinstance(service, disco.DiscoIdentity)] # FIXME: lang must be managed here
byte_identities.sort(key=lambda i: i.lang)
byte_identities.sort(key=lambda i: i.idType)
byte_identities.sort(key=lambda i: i.category)
for identity in byte_identities:
_s.append(str(identity))
_s.append('<')
byte_features = [service.encode('utf-8') for service in services if isinstance(service, disco.DiscoFeature)]
byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort
for feature in byte_features:
_s.append(feature)
_s.append('<')
#TODO: manage XEP-0128 data form here
XEP_0115.cap_hash = b64encode(sha1(''.join(_s)).digest())
debug(_('Capability hash generated: [%s]') % XEP_0115.cap_hash)
self.presenceHack(profile)
client.discoHandler.info(client.jid, client.jid, '').addCallback(generateHash_2, profile)
c_elt['node'] = C.APP_URL
c_elt['ver'] = cap_hash
XEP_0115.cap_hash = cap_hash
XEP_0115.c_elt = c_elt
if cap_hash not in self.host.memory.disco.hashes:
self.host.memory.disco.hashes[cap_hash] = disco_infos
self.host.memory.updateEntityData(client.jid, C.ENTITY_CAP_HASH, cap_hash, profile)
class XEP_0115_handler(XMPPHandler):
@ -163,38 +104,35 @@ class XEP_0115_handler(XMPPHandler):
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
return []
def _updateCache(self, discoResult, from_jid, key):
"""Actually update the cache
@param discoResult: result of the requestInfo"""
if key:
self.plugin_parent.jid_hash[from_jid] = key
self.plugin_parent.hash_cache[key] = discoResult.features
else:
#No key, that means unknown hash method
self.plugin_parent.jid_hash[from_jid] = discoResult.features
@defer.inlineCallbacks
def update(self, presence):
"""
Manage the capabilities of the entity
Check if we know the version of this capatilities
and get the capibilities if necessary
Check if we know the version of this capatilities and get the capibilities if necessary
"""
from_jid = jid.JID(presence['from'])
c_elem = filter(lambda x: x.name == "c", presence.elements())[0] # We only want the "c" element
c_elem = presence.elements(NS_ENTITY_CAPABILITY, 'c').next()
try:
ver = c_elem['ver']
hash = c_elem['hash']
node = c_elem['node']
c_ver = c_elem['ver']
c_hash = c_elem['hash']
c_node = c_elem['node']
except KeyError:
warning('Received invalid capabilities tag')
warning(_('Received invalid capabilities tag'))
return
if not from_jid in self.plugin_parent.jid_hash:
if ver in self.plugin_parent.hash_cache:
#we know that hash, we just link it with the jid
self.plugin_parent.jid_hash[from_jid] = ver
else:
if hash != 'sha-1':
#unknown hash method
warning('Unknown hash for entity capabilities: [%s]' % hash)
self.parent.disco.requestInfo(from_jid).addCallback(self._updateCache, from_jid, ver if hash == 'sha-1' else None)
#TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3
if c_ver in self.host.memory.disco.hashes:
# we already know the hash, we update the jid entity
debug ("hash [%s] already in cache, updating entity" % c_ver)
self.host.memory.updateEntityData(from_jid, C.ENTITY_CAP_HASH, c_ver, self.profile)
return
yield self.host.getDiscoInfos(from_jid, self.profile)
if c_hash != 'sha-1':
#unknown hash method
warning(_('Unknown hash method for entity capabilities: [%(hash_method)s] (entity: %(jid)s, node: %(node)s)') % {'hash_method':c_hash, 'jid': from_jid, 'node': c_node})
computed_hash = self.host.memory.getEntityDatum(from_jid, C.ENTITY_CAP_HASH, self.profile)
if computed_hash != c_ver:
warning(_('Computed hash differ from given hash:\ngiven: [%(given_hash)s]\ncomputed: [%(computed_hash)s]\n(entity: %(jid)s, node: %(node)s)') % {'given_hash':c_ver, 'computed_hash': computed_hash, 'jid': from_jid, 'node': c_node})
# TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3