# HG changeset patch # User Goffi # Date 1447787607 -3600 # Node ID 4dd07d0262144f069a5eba39c83446c8ef79c943 # Parent 3ec7511dbf2840b32cf5a4eb4dbd50345bf77c08 plugin XEP-0234: hash checksum proper handling diff -r 3ec7511dbf28 -r 4dd07d026214 src/plugins/plugin_xep_0234.py --- 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 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 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))