Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_xep_0234.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0234.py@2ced30f6d5de |
children | bc60875cb3b8 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/plugins/plugin_xep_0234.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,826 @@ +#!/usr/bin/env python3 + +# SàT plugin for Jingle File Transfer (XEP-0234) +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from collections import namedtuple +import mimetypes +import os.path + +from twisted.internet import defer +from twisted.internet import reactor +from twisted.internet import error as internet_error +from twisted.python import failure +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from twisted.words.xish import domish +from wokkel import disco, iwokkel +from zope.interface import implementer + +from libervia.backend.core import exceptions +from libervia.backend.core.constants import Const as C +from libervia.backend.core.i18n import D_, _ +from libervia.backend.core.log import getLogger +from libervia.backend.tools import utils +from libervia.backend.tools import stream +from libervia.backend.tools.common import date_utils +from libervia.backend.tools.common import regex + + +log = getLogger(__name__) + +NS_JINGLE_FT = "urn:xmpp:jingle:apps:file-transfer:5" + +PLUGIN_INFO = { + C.PI_NAME: "Jingle File Transfer", + C.PI_IMPORT_NAME: "XEP-0234", + C.PI_TYPE: "XEP", + C.PI_MODES: C.PLUG_MODE_BOTH, + C.PI_PROTOCOLS: ["XEP-0234"], + C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0300", "FILE"], + C.PI_MAIN: "XEP_0234", + C.PI_HANDLER: "yes", + C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), +} + +EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"} +Range = namedtuple("Range", ("offset", "length")) + + +class XEP_0234: + # 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 + # dependencies will be unloaded at the end) + Range = Range # we copy the class here, so it can be used by other plugins + name = PLUGIN_INFO[C.PI_NAME] + human_name = D_("file transfer") + + def __init__(self, host): + log.info(_("plugin Jingle File Transfer initialization")) + self.host = host + host.register_namespace("jingle-ft", NS_JINGLE_FT) + self._j = host.plugins["XEP-0166"] # shortcut to access jingle + self._j.register_application(NS_JINGLE_FT, self) + self._f = host.plugins["FILE"] + self._f.register(self, priority=10000) + self._hash = self.host.plugins["XEP-0300"] + host.bridge.add_method( + "file_jingle_send", + ".plugin", + in_sign="ssssa{ss}s", + out_sign="", + method=self._file_send, + async_=True, + ) + host.bridge.add_method( + "file_jingle_request", + ".plugin", + in_sign="sssssa{ss}s", + out_sign="s", + method=self._file_jingle_request, + async_=True, + ) + + def get_handler(self, client): + return XEP_0234_handler() + + def get_progress_id(self, session, content_name): + """Return a unique progress ID + + @param session(dict): jingle session + @param content_name(unicode): name of the content + @return (unicode): unique progress id + """ + return "{}_{}".format(session["id"], content_name) + + async def can_handle_file_send(self, client, peer_jid, filepath): + if peer_jid.resource: + return await self.host.hasFeature(client, NS_JINGLE_FT, peer_jid) + else: + # if we have a bare jid, Jingle Message Initiation will be tried + return True + + # generic methods + + def build_file_element( + self, client, name=None, file_hash=None, hash_algo=None, size=None, + mime_type=None, desc=None, modified=None, transfer_range=None, path=None, + namespace=None, file_elt=None, **kwargs): + """Generate a <file> element with available metadata + + @param file_hash(unicode, None): hash of the file + empty string to set <hash-used/> element + @param hash_algo(unicode, None): hash algorithm used + if file_hash is None and hash_algo is set, a <hash-used/> element will be + generated + @param transfer_range(Range, None): where transfer must start/stop + @param modified(int, unicode, None): date of last modification + 0 to use current date + int to use an unix timestamp + else must be an unicode string which will be used as it (it must be an XMPP + time) + @param file_elt(domish.Element, None): element to use + None to create a new one + @param **kwargs: data for plugin extension (ignored by default) + @return (domish.Element): generated element + @trigger XEP-0234_buildFileElement(file_elt, extra_args): can be used to extend + elements to add + """ + if file_elt is None: + file_elt = domish.Element((NS_JINGLE_FT, "file")) + for name, value in ( + ("name", name), + ("size", size), + ("media-type", mime_type), + ("desc", desc), + ("path", path), + ("namespace", namespace), + ): + if value is not None: + file_elt.addElement(name, content=str(value)) + + if modified is not None: + if isinstance(modified, int): + file_elt.addElement("date", utils.xmpp_date(modified or None)) + else: + file_elt.addElement("date", modified) + elif "created" in kwargs: + file_elt.addElement("date", utils.xmpp_date(kwargs.pop("created"))) + + range_elt = file_elt.addElement("range") + if transfer_range is not None: + if transfer_range.offset is not None: + range_elt["offset"] = transfer_range.offset + if transfer_range.length is not None: + range_elt["length"] = transfer_range.length + if file_hash is not None: + if not file_hash: + file_elt.addChild(self._hash.build_hash_used_elt()) + else: + file_elt.addChild(self._hash.build_hash_elt(file_hash, hash_algo)) + elif hash_algo is not None: + file_elt.addChild(self._hash.build_hash_used_elt(hash_algo)) + self.host.trigger.point( + "XEP-0234_buildFileElement", client, file_elt, extra_args=kwargs) + if kwargs: + for kw in kwargs: + log.debug("ignored keyword: {}".format(kw)) + return file_elt + + def build_file_element_from_dict(self, client, file_data, **kwargs): + """like build_file_element but get values from a file_data dict + + @param file_data(dict): metadata to use + @param **kwargs: data to override + """ + if kwargs: + file_data = file_data.copy() + file_data.update(kwargs) + try: + file_data["mime_type"] = ( + f'{file_data.pop("media_type")}/{file_data.pop("media_subtype")}' + ) + except KeyError: + pass + return self.build_file_element(client, **file_data) + + async def parse_file_element( + self, client, file_elt, file_data=None, given=False, parent_elt=None, + keep_empty_range=False): + """Parse a <file> element and file dictionary accordingly + + @param file_data(dict, None): dict where the data will be set + following keys will be set (and overwritten if they already exist): + name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range + if None, a new dict is created + @param given(bool): if True, prefix hash key with "given_" + @param parent_elt(domish.Element, None): parent of the file element + if set, file_elt must not be set + @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset + and length are None). + Empty range is useful to know if a peer_jid can handle range + @return (dict): file_data + @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new + elements + @raise exceptions.NotFound: there is not <file> element in parent_elt + @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT + """ + if parent_elt is not None: + if file_elt is not None: + raise exceptions.InternalError( + "file_elt must be None if parent_elt is set" + ) + try: + file_elt = next(parent_elt.elements(NS_JINGLE_FT, "file")) + except StopIteration: + raise exceptions.NotFound() + else: + if not file_elt or file_elt.uri != NS_JINGLE_FT: + raise exceptions.DataError( + "invalid <file> element: {stanza}".format(stanza=file_elt.toXml()) + ) + + if file_data is None: + file_data = {} + + for name in ("name", "desc", "path", "namespace"): + try: + file_data[name] = str(next(file_elt.elements(NS_JINGLE_FT, name))) + except StopIteration: + pass + + name = file_data.get("name") + if name == "..": + # we don't want to go to parent dir when joining to a path + name = "--" + file_data["name"] = name + elif name is not None and ("/" in name or "\\" in name): + file_data["name"] = regex.path_escape(name) + + try: + file_data["mime_type"] = str( + next(file_elt.elements(NS_JINGLE_FT, "media-type")) + ) + except StopIteration: + pass + + try: + file_data["size"] = int( + str(next(file_elt.elements(NS_JINGLE_FT, "size"))) + ) + except StopIteration: + pass + + try: + file_data["modified"] = date_utils.date_parse( + next(file_elt.elements(NS_JINGLE_FT, "date")) + ) + except StopIteration: + pass + + try: + range_elt = next(file_elt.elements(NS_JINGLE_FT, "range")) + except StopIteration: + pass + else: + offset = range_elt.getAttribute("offset") + length = range_elt.getAttribute("length") + if offset or length or keep_empty_range: + file_data["transfer_range"] = Range(offset=offset, length=length) + + prefix = "given_" if given else "" + hash_algo_key, hash_key = "hash_algo", prefix + "file_hash" + try: + file_data[hash_algo_key], file_data[hash_key] = self._hash.parse_hash_elt( + file_elt + ) + except exceptions.NotFound: + pass + + self.host.trigger.point("XEP-0234_parseFileElement", client, file_elt, file_data) + + return file_data + + # bridge methods + + def _file_send( + self, + peer_jid, + filepath, + name="", + file_desc="", + extra=None, + profile=C.PROF_KEY_NONE, + ): + client = self.host.get_client(profile) + return defer.ensureDeferred(self.file_send( + client, + jid.JID(peer_jid), + filepath, + name or None, + file_desc or None, + extra or None, + )) + + async def file_send( + self, client, peer_jid, filepath, name, file_desc=None, extra=None + ): + """Send a file using jingle file transfer + + @param peer_jid(jid.JID): destinee jid + @param filepath(str): absolute path of the file + @param name(unicode, None): name of the file + @param file_desc(unicode, None): description of the file + @return (D(unicode)): progress id + """ + progress_id_d = defer.Deferred() + if extra is None: + extra = {} + if file_desc is not None: + extra["file_desc"] = file_desc + encrypted = extra.pop("encrypted", False) + await self._j.initiate( + client, + peer_jid, + [ + { + "app_ns": NS_JINGLE_FT, + "senders": self._j.ROLE_INITIATOR, + "app_kwargs": { + "filepath": filepath, + "name": name, + "extra": extra, + "progress_id_d": progress_id_d, + }, + } + ], + encrypted = encrypted + ) + return await progress_id_d + + def _file_jingle_request( + self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, + profile=C.PROF_KEY_NONE): + client = self.host.get_client(profile) + return defer.ensureDeferred(self.file_jingle_request( + client, + jid.JID(peer_jid), + filepath, + name or None, + file_hash or None, + hash_algo or None, + extra or None, + )) + + async def file_jingle_request( + self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None, + extra=None): + """Request a file using jingle file transfer + + @param peer_jid(jid.JID): destinee jid + @param filepath(str): absolute path where the file will be downloaded + @param name(unicode, None): name of the file + @param file_hash(unicode, None): hash of the file + @return (D(unicode)): progress id + """ + progress_id_d = defer.Deferred() + if extra is None: + extra = {} + if file_hash is not None: + if hash_algo is None: + raise ValueError(_("hash_algo must be set if file_hash is set")) + extra["file_hash"] = file_hash + extra["hash_algo"] = hash_algo + else: + if hash_algo is not None: + raise ValueError(_("file_hash must be set if hash_algo is set")) + await self._j.initiate( + client, + peer_jid, + [ + { + "app_ns": NS_JINGLE_FT, + "senders": self._j.ROLE_RESPONDER, + "app_kwargs": { + "filepath": filepath, + "name": name, + "extra": extra, + "progress_id_d": progress_id_d, + }, + } + ], + ) + return await progress_id_d + + # jingle callbacks + + def jingle_description_elt( + self, client, session, content_name, filepath, name, extra, progress_id_d + ): + return domish.Element((NS_JINGLE_FT, "description")) + + def jingle_session_init( + self, client, session, content_name, filepath, name, extra, progress_id_d + ): + if extra is None: + extra = {} + else: + if not EXTRA_ALLOWED.issuperset(extra): + raise ValueError( + _("only the following keys are allowed in extra: {keys}").format( + keys=", ".join(EXTRA_ALLOWED) + ) + ) + progress_id_d.callback(self.get_progress_id(session, content_name)) + content_data = session["contents"][content_name] + application_data = content_data["application_data"] + assert "file_path" not in application_data + application_data["file_path"] = filepath + file_data = application_data["file_data"] = {} + desc_elt = self.jingle_description_elt( + client, session, content_name, filepath, name, extra, progress_id_d) + file_elt = desc_elt.addElement("file") + + if content_data["senders"] == self._j.ROLE_INITIATOR: + # we send a file + if name is None: + name = os.path.basename(filepath) + file_data["date"] = utils.xmpp_date() + file_data["desc"] = extra.pop("file_desc", "") + file_data["name"] = name + mime_type = mimetypes.guess_type(name, strict=False)[0] + if mime_type is not None: + file_data["mime_type"] = mime_type + file_data["size"] = os.path.getsize(filepath) + if "namespace" in extra: + file_data["namespace"] = extra["namespace"] + if "path" in extra: + file_data["path"] = extra["path"] + self.build_file_element_from_dict( + client, file_data, file_elt=file_elt, file_hash="") + else: + # we request a file + file_hash = extra.pop("file_hash", "") + if not name and not file_hash: + raise ValueError(_("you need to provide at least name or file hash")) + if name: + file_data["name"] = name + if file_hash: + file_data["file_hash"] = file_hash + file_data["hash_algo"] = extra["hash_algo"] + else: + file_data["hash_algo"] = self._hash.get_default_algo() + if "namespace" in extra: + file_data["namespace"] = extra["namespace"] + if "path" in extra: + file_data["path"] = extra["path"] + self.build_file_element_from_dict(client, file_data, file_elt=file_elt) + + return desc_elt + + async def jingle_request_confirmation( + self, client, action, session, content_name, desc_elt + ): + """This method request confirmation for a jingle session""" + content_data = session["contents"][content_name] + senders = content_data["senders"] + if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER): + log.warning("Bad sender, assuming initiator") + senders = content_data["senders"] = self._j.ROLE_INITIATOR + # first we grab file informations + try: + file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file")) + except StopIteration: + raise failure.Failure(exceptions.DataError) + file_data = {"progress_id": self.get_progress_id(session, content_name)} + + if senders == self._j.ROLE_RESPONDER: + # we send the file + return await self._file_sending_request_conf( + client, session, content_data, content_name, file_data, file_elt + ) + else: + # we receive the file + return await self._file_receiving_request_conf( + client, session, content_data, content_name, file_data, file_elt + ) + + async def _file_sending_request_conf( + self, client, session, content_data, content_name, file_data, file_elt + ): + """parse file_elt, and handle file retrieving/permission checking""" + await self.parse_file_element(client, file_elt, file_data) + content_data["application_data"]["file_data"] = file_data + finished_d = content_data["finished_d"] = defer.Deferred() + + # confirmed_d is a deferred returning confimed value (only used if cont is False) + cont, confirmed_d = self.host.trigger.return_point( + "XEP-0234_fileSendingRequest", + client, + session, + content_data, + content_name, + file_data, + file_elt, + ) + if not cont: + confirmed = await confirmed_d + if confirmed: + args = [client, session, content_name, content_data] + finished_d.addCallbacks( + self._finished_cb, self._finished_eb, args, None, args + ) + return confirmed + + log.warning(_("File continue is not implemented yet")) + return False + + async def _file_receiving_request_conf( + self, client, session, content_data, content_name, file_data, file_elt + ): + """parse file_elt, and handle user permission/file opening""" + await self.parse_file_element(client, file_elt, file_data, given=True) + try: + hash_algo, file_data["given_file_hash"] = self._hash.parse_hash_elt(file_elt) + except exceptions.NotFound: + try: + hash_algo = self._hash.parse_hash_used_elt(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.get_hasher(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) + + name = file_data["name"] + if "/" in name or "\\" in name: + log.warning( + "File name contain path characters, we replace them: {}".format(name) + ) + file_data["name"] = name.replace("/", "_").replace("\\", "_") + + content_data["application_data"]["file_data"] = file_data + + # now we actualy request permission to user + + # deferred to track end of transfer + finished_d = content_data["finished_d"] = defer.Deferred() + confirmed = await self._f.get_dest_dir( + client, session["peer_jid"], content_data, file_data, stream_object=True + ) + if confirmed: + await self.host.trigger.async_point( + "XEP-0234_file_receiving_request_conf", + client, session, content_data, file_elt + ) + args = [client, session, content_name, content_data] + finished_d.addCallbacks( + self._finished_cb, self._finished_eb, args, None, args + ) + return confirmed + + async def jingle_handler(self, client, action, session, content_name, desc_elt): + content_data = session["contents"][content_name] + application_data = content_data["application_data"] + if action in (self._j.A_ACCEPTED_ACK,): + pass + elif action == self._j.A_SESSION_INITIATE: + file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file")) + try: + next(file_elt.elements(NS_JINGLE_FT, "range")) + except StopIteration: + # initiator doesn't manage <range>, but we do so we advertise it + # FIXME: to be checked + log.debug("adding <range> element") + file_elt.addElement("range") + elif action == self._j.A_SESSION_ACCEPT: + assert not "stream_object" in content_data + file_data = application_data["file_data"] + file_path = application_data["file_path"] + senders = content_data["senders"] + if senders != session["role"]: + # we are receiving the file + try: + # did the responder specified the size of the file? + file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file")) + size_elt = next(file_elt.elements(NS_JINGLE_FT, "size")) + size = int(str(size_elt)) + except (StopIteration, ValueError): + size = None + # XXX: hash security is not critical here, so we just take the higher + # mandatory one + hasher = file_data["hash_hasher"] = self._hash.get_hasher() + progress_id = self.get_progress_id(session, content_name) + try: + content_data["stream_object"] = stream.FileStreamObject( + self.host, + client, + file_path, + mode="wb", + uid=progress_id, + size=size, + data_cb=lambda data: hasher.update(data), + ) + except Exception as e: + self.host.bridge.progress_error( + progress_id, C.PROGRESS_ERROR_FAILED, client.profile + ) + await self._j.terminate( + client, self._j.REASON_FAILED_APPLICATION, session) + raise e + else: + # we are sending the file + 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.get_hasher() + content_data["stream_object"] = stream.FileStreamObject( + self.host, + client, + file_path, + uid=self.get_progress_id(session, content_name), + size=size, + data_cb=lambda data: hasher.update(data), + ) + finished_d = content_data["finished_d"] = defer.Deferred() + args = [client, session, content_name, content_data] + finished_d.addCallbacks(self._finished_cb, self._finished_eb, args, None, args) + await self.host.trigger.async_point( + "XEP-0234_jingle_handler", + client, session, content_data, desc_elt + ) + else: + log.warning("FIXME: unmanaged action {}".format(action)) + return desc_elt + + def jingle_session_info(self, client, action, session, content_name, jingle_elt): + """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( + "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 = next(elt.elements(NS_JINGLE_FT, "file")) + except StopIteration: + raise exceptions.DataError + algo, file_data["given_file_hash"] = self._hash.parse_hash_elt(file_elt) + if algo != file_data.get("hash_algo"): + log.warning( + "Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo}) [{profile}]".format( + peer_algo=algo, + our_algo=file_data.get("hash_algo"), + profile=client.profile, + ) + ) + else: + self._receiver_try_terminate( + client, session, content_name, content_data + ) + else: + raise NotImplementedError + + def jingle_terminate(self, client, action, session, content_name, reason_elt): + if reason_elt.decline: + # progress is the only way to tell to frontends that session has been declined + progress_id = self.get_progress_id(session, content_name) + self.host.bridge.progress_error( + progress_id, C.PROGRESS_ERROR_DECLINED, client.profile + ) + elif not reason_elt.success: + progress_id = self.get_progress_id(session, content_name) + first_child = reason_elt.firstChildElement() + if first_child is not None: + reason = first_child.name + if reason_elt.text is not None: + reason = f"{reason} - {reason_elt.text}" + else: + reason = C.PROGRESS_ERROR_FAILED + self.host.bridge.progress_error( + progress_id, reason, client.profile + ) + + def _send_check_sum(self, client, session, content_name, content_data): + """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("Calculated hash: {}".format(hash_)) + iq_elt, jingle_elt = self._j.build_session_info(client, session) + 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.build_hash_elt(hash_)) + iq_elt.send() + + def _receiver_try_terminate( + self, client, session, content_name, content_data, last_try=False + ): + """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"] + given_hash = file_data.get("given_file_hash") + if given_hash is None: + if last_try: + log.warning( + "sender didn't sent hash checksum, we can't check the file [{profile}]".format( + profile=client.profile + ) + ) + self._j.delayed_content_terminate(client, session, content_name) + content_data["stream_object"].close() + return True + return False + hasher = file_data["hash_hasher"] + hash_ = hasher.hexdigest() + + if hash_ == given_hash: + log.info(f"Hash checked, file was successfully transfered: {hash_}") + progress_metadata = { + "hash": hash_, + "hash_algo": file_data["hash_algo"], + "hash_verified": C.BOOL_TRUE, + } + error = None + else: + log.warning("Hash mismatch, the file was not transfered correctly") + progress_metadata = None + error = "Hash mismatch: given={algo}:{given}, calculated={algo}:{our}".format( + algo=file_data["hash_algo"], given=given_hash, our=hash_ + ) + + self._j.delayed_content_terminate(client, session, content_name) + content_data["stream_object"].close(progress_metadata, error) + # 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 _finished_cb(self, __, client, session, content_name, content_data): + log.info("File transfer terminated") + 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._receiver_try_terminate( + client, session, content_name, content_data + ): + # we have not received the hash yet, we wait 5 more seconds + content_data["last_try_timer"] = reactor.callLater( + 5, + self._receiver_try_terminate, + client, + session, + content_name, + content_data, + last_try=True, + ) + else: + # we are the sender, we send the checksum + self._send_check_sum(client, session, content_name, content_data) + content_data["stream_object"].close() + + def _finished_eb(self, failure, client, session, content_name, content_data): + log.warning("Error while streaming file: {}".format(failure)) + content_data["stream_object"].close() + self._j.content_terminate( + client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT + ) + + +@implementer(iwokkel.IDisco) +class XEP_0234_handler(XMPPHandler): + + def getDiscoInfo(self, requestor, target, nodeIdentifier=""): + return [disco.DiscoFeature(NS_JINGLE_FT)] + + def getDiscoItems(self, requestor, target, nodeIdentifier=""): + return []