changeset 1618:0de5f210fe56

plugin XEP-0300: implemented hashing: - parseHashElt do what it says - calculateHashElt can calculate a whole hash in a non-blocking way - or getHasher can be used for progressive hashing - getBestPeerAlgo return the best available hashing algorithm of an entity
author Goffi <goffi@goffi.org>
date Tue, 17 Nov 2015 19:48:19 +0100
parents d05f9179fe22
children 3ec7511dbf28
files src/plugins/plugin_xep_0234.py src/plugins/plugin_xep_0300.py
diffstat 2 files changed, 126 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0234.py	Tue Nov 17 19:41:30 2015 +0100
+++ b/src/plugins/plugin_xep_0234.py	Tue Nov 17 19:48:19 2015 +0100
@@ -58,6 +58,7 @@
         self._j.registerApplication(NS_JINGLE_FT, self)
         self._f = host.plugins["FILE"]
         self._f.register(NS_JINGLE_FT, self.fileJingleSend, priority = 10000, method_name=u"Jingle")
+        self._hash = self.host.plugins["XEP-0300"]
         host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='sssss', out_sign='', method=self._fileJingleSend)
 
     def getHandler(self, profile):
@@ -116,7 +117,7 @@
         for name in ('date', 'desc', 'media-type', 'name', 'size'):
             file_elt.addElement(name, content=unicode(file_data[name]))
         file_elt.addElement("range") # TODO
-        file_elt.addChild(self.host.plugins["XEP-0300"].buidHash())
+        file_elt.addChild(self._hash.buildHashElt())
         return desc_elt
 
     def jingleRequestConfirmation(self, action, session, content_name, desc_elt, profile):
--- a/src/plugins/plugin_xep_0300.py	Tue Nov 17 19:41:30 2015 +0100
+++ b/src/plugins/plugin_xep_0300.py	Tue Nov 17 19:48:19 2015 +0100
@@ -20,44 +20,155 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
+from sat.core import exceptions
 from twisted.words.xish import domish
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from twisted.internet import threads
+from twisted.internet import defer
+from zope.interface import implements
+from wokkel import disco, iwokkel
+from collections import OrderedDict
 import hashlib
 
 
-NS_HASHES = "urn:xmpp:hashes:1"
-
 PLUGIN_INFO = {
     "name": "Cryptographic Hash Functions",
     "import_name": "XEP-0300",
     "type": "XEP",
     "protocols": ["XEP-0300"],
     "main": "XEP_0300",
-    "handler": "no",
+    "handler": "yes",
     "description": _("""Management of cryptographic hashes""")
 }
 
+NS_HASHES = "urn:xmpp:hashes:1"
+NS_HASHES_FUNCTIONS = u"urn:xmpp:hash-function-text-names:{}"
+BUFFER_SIZE = 2**12
+ALGO_DEFAULT = 'sha-256'
+
 
 class XEP_0300(object):
-    ALGOS = {u'md5': hashlib.md5,
-             u'sha-1': hashlib.sha1,
-             u'sha-256': hashlib.sha256,
-             u'sha-512': hashlib.sha512,
-            }
+    ALGOS = OrderedDict((
+            (u'md5', hashlib.md5),
+            (u'sha-1', hashlib.sha1),
+            (u'sha-256', hashlib.sha256),
+            (u'sha-512', hashlib.sha512),
+            ))
 
     def __init__(self, host):
         log.info(_("plugin Hashes initialization"))
 
-    def buidHash(self, file_obj=None, algo='sha-256'):
+    def getHandler(self, profile):
+        return XEP_0300_handler()
+
+    def getHasher(self, algo):
+        """Return hasher instance
+
+         /!\\ blocking method, considere using calculateHashElt
+         if you want to hash a big file
+         @param algo(unicode): one of the XEP_300.ALGOS keys
+         @return (hash object): same object s in hashlib.
+            update method need to be called for each chunh
+            diget or hexdigest can be used at the end
+        """
+        return self.ALGOS[algo]()
+
+    @defer.inlineCallbacks
+    def getBestPeerAlgo(self, to_jid, profile):
+        """Return the best available hashing algorith of other peer
+
+         @param to_jid(jid.JID): peer jid
+         @parm profile: %(doc_profile)s
+         @return (D(unicode, None)): best available algorithm,
+            or None if hashing is not possible
+        """
+        for algo in reversed(XEP_0300.ALGOS):
+            has_feature = yield self.host.hasFeature(NS_HASHES_FUNCTIONS.format(algo), to_jid, profile)
+            if has_feature:
+                log.debug(u"Best hashing algorithm found for {jid}: {algo}".format(
+                    jid=to_jid.full(),
+                    algo=algo))
+                defer.returnValue(algo)
+
+    def calculateHashBlocking(self, file_obj, hasher):
+        """Calculate hash in a blocking way
+
+        @param file_obj(file): a file-like object
+        @param hasher(callable): the method to call to initialise hash object
+        @return (str): the hex digest of the hash
+        """
+        hash_ = hasher()
+        while True:
+            buf = file_obj.read(BUFFER_SIZE)
+            if not buf:
+                break
+            hash_.update(buf)
+        return hash_.hexdigest()
+
+    def calculateHashElt(self, file_obj=None, algo=ALGO_DEFAULT):
         """Compute hash and build hash element
 
-        @param file_obj(file, None): file to use to calculate the hash
-            if file_obj is None, en empty hash element will be returned (useful e.g. in XEP-0234)
+        @param file_obj(file, None): file-like object to use to calculate the hash
+        @param algo(unicode): algorithme to use, must be a key of XEP_0300.ALGOS
+        @return (D(domish.Element)): hash element
+        """
+        def hashCalculated(hash_):
+            return self.buildHashElt(hash_, algo)
+        hasher = self.ALGOS[algo]
+        hash_d = threads.deferToThread(self.calculateHashBlocking, file_obj, hasher)
+        hash_d.addCallback(hashCalculated)
+        return hash_d
+
+    def buildHashElt(self, hash_=None, algo=ALGO_DEFAULT):
+        """Compute hash and build hash element
+
+        @param hash_(None, str): hash to use, or None for an empty element
         @param algo(unicode): algorithme to use, must be a key of XEP_0300.ALGOS
         @return (domish.Element): computed hash
         """
-        hasher = self.ALGOS[algo]
         hash_elt = domish.Element((NS_HASHES, 'hash'))
+        if hash_ is not None:
+            hash_elt.addContent(hash_)
         hash_elt['algo']=algo
-        # TODO: actually hash, use deferToThread
         return hash_elt
 
+    def parseHashElt(self, parent):
+        """Find and parse a hash element
+
+        if multiple elements are found, the strongest managed one is returned
+        @param (domish.Element): parent of <hash/> element
+        @return (tuple[unicode, str]): (algo, hash) tuple
+            both values can be None if <hash/> is empty
+        @raise exceptions.NotFound: the element is not present
+        """
+        algos = XEP_0300.ALGOS.keys()
+        hash_elt = None
+        best_algo = None
+        best_value = None
+        for hash_elt in parent.elements(NS_HASHES, 'hash'):
+            algo = hash_elt.getAttribute('algo')
+            try:
+                idx = algos.index(algo)
+            except ValueError:
+                log.warning(u"Proposed {} algorithm is not managed".format(algo))
+                algo = None
+                continue
+
+            if best_algo is None or algos.index(best_algo) < idx:
+                best_algo = algo
+                best_value = str(hash_elt) or None
+
+        if not hash_elt:
+            raise exceptions.NotFound
+        return best_algo, best_value
+
+
+class XEP_0300_handler(XMPPHandler):
+    implements(iwokkel.IDisco)
+
+    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
+        hash_functions_names = [disco.DiscoFeature(NS_HASHES_FUNCTIONS.format(algo)) for algo in XEP_0300.ALGOS]
+        return [disco.DiscoFeature(NS_HASHES)] + hash_functions_names
+
+    def getDiscoItems(self, requestor, target, nodeIdentifier=''):
+        return []