diff --git a/src/plugins/plugin_xep_0234.py b/src/plugins/plugin_xep_0234.py
index 88b3f124..e7d2554a 100644
--- a/src/plugins/plugin_xep_0234.py
+++ b/src/plugins/plugin_xep_0234.py
@@ -31,6 +31,8 @@ from twisted.words.protocols.jabber import jid
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 @@ PLUGIN_INFO = {
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"))
@@ -138,6 +140,16 @@ class XEP_0234(object):
except StopIteration:
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:
@@ -148,8 +160,6 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
log.warning(u"FIXME: unmanaged action {}".format(action))
return desc_elt
- def _finishedCb(self, dummy, session, content_name, content_data, profile):
- log.info(u"File transfer completed successfuly")
- if content_data['senders'] != session['role']:
- # we terminate the session only if we are the received,
- # as recommanded in XEP-0234 §2 (after example 6)
- self._j.contentTerminate(session, content_name, profile=profile)
+ def jingleSessionInfo(self, action, session, content_name, jingle_elt, profile):
+ """Called on session-info action
+
+ manage checksum, and ignore element
+ """
+ # TODO: manage 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")
+ if content_data['senders'] != session['role']:
+ # we terminate the session only if we are the receiver,
+ # as recommanded in XEP-0234 §2 (after example 6)
+ 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))