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 []