changeset 1620:4dd07d026214

plugin XEP-0234: hash checksum proper handling
author Goffi <goffi@goffi.org>
date Tue, 17 Nov 2015 20:13:27 +0100 (2015-11-17)
parents 3ec7511dbf28
children a17a91531fbe
files src/plugins/plugin_xep_0234.py
diffstat 1 files changed, 116 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0234.py	Tue Nov 17 19:51:52 2015 +0100
+++ b/src/plugins/plugin_xep_0234.py	Tue Nov 17 20:13:27 2015 +0100
@@ -31,6 +31,8 @@
 from twisted.python import failure
 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
 from twisted.internet import defer
+from twisted.internet import reactor
+from twisted.internet import error as internet_error
 
 
 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4'
@@ -49,7 +51,7 @@
 
 class XEP_0234(object):
     # TODO: assure everything is closed when file is sent or session terminate is received
-    # TODO: call self._f.unregister when unloading order will be managing (i.e. when depenencies will be unloaded at the end)
+    # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end)
 
     def __init__(self, host):
         log.info(_("plugin Jingle File Transfer initialization"))
@@ -139,6 +141,16 @@
                 file_data[name] = ''
 
         try:
+            hash_algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt)
+        except exceptions.NotFound:
+            raise failure.Failure(exceptions.DataError)
+
+        if hash_algo is not None:
+            file_data['hash_algo'] = hash_algo
+            file_data['hash_hasher'] = hasher = self._hash.getHasher(hash_algo)
+            file_data['data_cb'] = lambda data: hasher.update(data)
+
+        try:
             file_data['size'] = int(file_data['size'])
         except ValueError:
             raise failure.Failure(exceptions.DataError)
@@ -148,8 +160,6 @@
             log.warning(u"File name contain path characters, we replace them: {}".format(name))
             file_data['name'] = name.replace('/', '_').replace('\\', '_')
 
-        # TODO: parse hash using plugin XEP-0300
-
         content_data['application_data']['file_data'] = file_data
 
         # now we actualy request permission to user
@@ -164,7 +174,6 @@
         d.addCallback(gotConfirmation)
         return d
 
-
     def jingleHandler(self, action, session, content_name, desc_elt, profile):
         content_data = session['contents'][content_name]
         application_data = content_data['application_data']
@@ -180,12 +189,16 @@
                 file_elt.addElement('range')
         elif action == self._j.A_SESSION_ACCEPT:
             assert not 'file_obj' in content_data
+            file_data = application_data['file_data']
             file_path = application_data['file_path']
-            size = application_data['file_data']['size']
+            size = file_data['size']
+            # XXX: hash security is not critical here, so we just take the higher mandatory one
+            hasher = file_data['hash_hasher'] = self._hash.getHasher('sha-256')
             content_data['file_obj'] = self._f.File(self.host,
                                        file_path,
                                        uid=self._getProgressId(session, content_name),
                                        size=size,
+                                       data_cb=lambda data: hasher.update(data),
                                        profile=profile
                                        )
             finished_d = content_data['finished_d'] = defer.Deferred()
@@ -195,13 +208,107 @@
             log.warning(u"FIXME: unmanaged action {}".format(action))
         return desc_elt
 
+    def jingleSessionInfo(self, action, session, content_name, jingle_elt, profile):
+        """Called on session-info action
+
+        manage checksum, and ignore <received/> element
+        """
+        # TODO: manage <received/> element
+        content_data = session['contents'][content_name]
+        elts = [elt for elt in jingle_elt.elements() if elt.uri == NS_JINGLE_FT]
+        if not elts:
+            return
+        for elt in elts:
+            if elt.name == 'received':
+                pass
+            elif elt.name == 'checksum':
+                # we have received the file hash, we need to parse it
+                if content_data['senders'] == session['role']:
+                    log.warning(u"unexpected checksum received while we are the file sender")
+                    raise exceptions.DataError
+                info_content_name = elt['name']
+                if info_content_name != content_name:
+                    # it was for an other content...
+                    return
+                file_data = content_data['application_data']['file_data']
+                try:
+                    file_elt = elt.elements((NS_JINGLE_FT, 'file')).next()
+                except StopIteration:
+                    raise exceptions.DataError
+                algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt)
+                if algo != file_data.get('hash_algo'):
+                    log.warning(u"Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo})"
+                        .format(peer_algo=algo, our_algo=file_data.get('hash_algo')))
+                else:
+                    self._receiverTryTerminate(session, content_name, content_data, profile=profile)
+            else:
+                raise NotImplementedError
+
+    def _sendCheckSum(self, session, content_name, content_data, profile):
+        """Send the session-info with the hash checksum"""
+        file_data = content_data['application_data']['file_data']
+        hasher = file_data['hash_hasher']
+        hash_ = hasher.hexdigest()
+        log.debug(u"Calculated hash: {}".format(hash_))
+        iq_elt, jingle_elt = self._j.buildSessionInfo(session, profile)
+        checksum_elt = jingle_elt.addElement((NS_JINGLE_FT, 'checksum'))
+        checksum_elt['creator'] = content_data['creator']
+        checksum_elt['name'] = content_name
+        file_elt = checksum_elt.addElement('file')
+        file_elt.addChild(self._hash.buildHashElt(hash_))
+        iq_elt.send()
+
+    def _receiverTryTerminate(self, session, content_name, content_data, last_try=False, profile=C.PROF_KEY_NONE):
+        """Try to terminate the session
+
+        This method must only be used by the receiver.
+        It check if transfer is finished, and hash available,
+        if everything is OK, it check hash and terminate the session
+        @param last_try(bool): if True this mean than session must be terminated even given hash is not available
+        @return (bool): True if session was terminated
+        """
+        if not content_data.get('transfer_finished', False):
+            return False
+        file_data = content_data['application_data']['file_data']
+        hash_given = file_data.get('hash_given')
+        if hash_given is None:
+            if last_try:
+                log.warning(u"sender didn't sent hash checksum, we can't check the file")
+                self._j.delayedContentTerminate(session, content_name, profile=profile)
+                content_data['file_obj'].close()
+                return True
+            return False
+        hasher = file_data['hash_hasher']
+        hash_ = hasher.hexdigest()
+
+        if hash_ == hash_given:
+            log.info(u"Hash checked, file was successfully transfered: {}".format(hash_))
+        else:
+            log.warning(u"Hash mismatch, the file was not transfered correctly")
+
+        self._j.delayedContentTerminate(session, content_name, profile=profile)
+        content_data['file_obj'].close()
+        # we may have the last_try timer still active, so we try to cancel it
+        try:
+            content_data['last_try_timer'].cancel()
+        except (KeyError, internet_error.AlreadyCalled):
+            pass
+        return True
+
     def _finishedCb(self, dummy, session, content_name, content_data, profile):
-        log.info(u"File transfer completed successfuly")
+        log.info(u"File transfer completed")
         if content_data['senders'] != session['role']:
-            # we terminate the session only if we are the received,
+            # we terminate the session only if we are the receiver,
             # as recommanded in XEP-0234 ยง2 (after example 6)
-            self._j.contentTerminate(session, content_name, profile=profile)
-        content_data['file_obj'].close()
+            content_data['transfer_finished'] = True
+            if not self._receiverTryTerminate(session, content_name, content_data, profile=profile):
+                # we have not received the hash yet, we wait 5 more seconds
+                content_data['last_try_timer'] = reactor.callLater(
+                    5, self._receiverTryTerminate, session, content_name, content_data, last_try=True, profile=profile)
+        else:
+            # we are the sender, we send the checksum
+            self._sendCheckSum(session, content_name, content_data, profile)
+            content_data['file_obj'].close()
 
     def _finishedEb(self, failure, session, content_name, content_data, profile):
         log.warning(u"Error while streaming through s5b: {}".format(failure))