- a new `XEP-0166_initiate` async trigger point is available - `initate` is now a coroutine - `jingleSessionInit` in applications and transports handlers are now called using `utils.adDeferred` - new `getApplication` helper method, to retrieve application from its namespace - new `getContentData` helper method to retrieve application and its argument from content
815 lines
32 KiB
Python
815 lines
32 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# SàT plugin for Jingle File Transfer (XEP-0234)
|
|
# Copyright (C) 2009-2020 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/>.
|
|
|
|
import os.path
|
|
import mimetypes
|
|
from collections import namedtuple
|
|
from zope.interface import implementer
|
|
from twisted.words.xish import domish
|
|
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
|
|
from wokkel import disco, iwokkel
|
|
from sat.core.i18n import _, D_
|
|
from sat.core.constants import Const as C
|
|
from sat.core.log import getLogger
|
|
from sat.core import exceptions
|
|
from sat.tools import utils
|
|
from sat.tools import stream
|
|
from sat.tools.common import date_utils
|
|
from sat.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.registerNamespace("jingle-ft", NS_JINGLE_FT)
|
|
self._j = host.plugins["XEP-0166"] # shortcut to access jingle
|
|
self._j.registerApplication(NS_JINGLE_FT, self)
|
|
self._f = host.plugins["FILE"]
|
|
self._f.register(self, priority=10000)
|
|
self._hash = self.host.plugins["XEP-0300"]
|
|
host.bridge.addMethod(
|
|
"fileJingleSend",
|
|
".plugin",
|
|
in_sign="ssssa{ss}s",
|
|
out_sign="",
|
|
method=self._fileSend,
|
|
async_=True,
|
|
)
|
|
host.bridge.addMethod(
|
|
"fileJingleRequest",
|
|
".plugin",
|
|
in_sign="sssssa{ss}s",
|
|
out_sign="s",
|
|
method=self._fileJingleRequest,
|
|
async_=True,
|
|
)
|
|
|
|
def getHandler(self, client):
|
|
return XEP_0234_handler()
|
|
|
|
def getProgressId(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 canHandleFileSend(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 buildFileElement(
|
|
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.buildHashUsedElt())
|
|
else:
|
|
file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo))
|
|
elif hash_algo is not None:
|
|
file_elt.addChild(self._hash.buildHashUsedElt(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 buildFileElementFromDict(self, client, file_data, **kwargs):
|
|
"""like buildFileElement 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.buildFileElement(client, **file_data)
|
|
|
|
async def parseFileElement(
|
|
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.pathEscape(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.parseHashElt(
|
|
file_elt
|
|
)
|
|
except exceptions.NotFound:
|
|
pass
|
|
|
|
self.host.trigger.point("XEP-0234_parseFileElement", client, file_elt, file_data)
|
|
|
|
return file_data
|
|
|
|
# bridge methods
|
|
|
|
def _fileSend(
|
|
self,
|
|
peer_jid,
|
|
filepath,
|
|
name="",
|
|
file_desc="",
|
|
extra=None,
|
|
profile=C.PROF_KEY_NONE,
|
|
):
|
|
client = self.host.getClient(profile)
|
|
return defer.ensureDeferred(self.fileSend(
|
|
client,
|
|
jid.JID(peer_jid),
|
|
filepath,
|
|
name or None,
|
|
file_desc or None,
|
|
extra or None,
|
|
))
|
|
|
|
async def fileSend(
|
|
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
|
|
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,
|
|
},
|
|
}
|
|
],
|
|
)
|
|
return await progress_id_d
|
|
|
|
def _fileJingleRequest(
|
|
self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,
|
|
profile=C.PROF_KEY_NONE):
|
|
client = self.host.getClient(profile)
|
|
return self.fileJingleRequest(
|
|
client,
|
|
jid.JID(peer_jid),
|
|
filepath,
|
|
name or None,
|
|
file_hash or None,
|
|
hash_algo or None,
|
|
extra or None,
|
|
)
|
|
|
|
@defer.inlineCallbacks
|
|
def fileJingleRequest(
|
|
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"))
|
|
yield 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,
|
|
},
|
|
}
|
|
],
|
|
)
|
|
progress_id = yield progress_id_d
|
|
defer.returnValue(progress_id)
|
|
|
|
# jingle callbacks
|
|
|
|
def jingleDescriptionElt(
|
|
self, client, session, content_name, filepath, name, extra, progress_id_d
|
|
):
|
|
return domish.Element((NS_JINGLE_FT, "description"))
|
|
|
|
def jingleSessionInit(
|
|
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.getProgressId(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.jingleDescriptionElt(
|
|
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.buildFileElementFromDict(
|
|
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.getDefaultAlgo()
|
|
if "namespace" in extra:
|
|
file_data["namespace"] = extra["namespace"]
|
|
if "path" in extra:
|
|
file_data["path"] = extra["path"]
|
|
self.buildFileElementFromDict(client, file_data, file_elt=file_elt)
|
|
|
|
return desc_elt
|
|
|
|
async def jingleRequestConfirmation(
|
|
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.getProgressId(session, content_name)}
|
|
|
|
if senders == self._j.ROLE_RESPONDER:
|
|
# we send the file
|
|
return await self._fileSendingRequestConf(
|
|
client, session, content_data, content_name, file_data, file_elt
|
|
)
|
|
else:
|
|
# we receive the file
|
|
return await self._fileReceivingRequestConf(
|
|
client, session, content_data, content_name, file_data, file_elt
|
|
)
|
|
|
|
async def _fileSendingRequestConf(
|
|
self, client, session, content_data, content_name, file_data, file_elt
|
|
):
|
|
"""parse file_elt, and handle file retrieving/permission checking"""
|
|
await self.parseFileElement(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.returnPoint(
|
|
"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._finishedCb, self._finishedEb, args, None, args
|
|
)
|
|
return confirmed
|
|
|
|
log.warning(_("File continue is not implemented yet"))
|
|
return False
|
|
|
|
async def _fileReceivingRequestConf(
|
|
self, client, session, content_data, content_name, file_data, file_elt
|
|
):
|
|
"""parse file_elt, and handle user permission/file opening"""
|
|
await self.parseFileElement(client, file_elt, file_data, given=True)
|
|
try:
|
|
hash_algo, file_data["given_file_hash"] = self._hash.parseHashElt(file_elt)
|
|
except exceptions.NotFound:
|
|
try:
|
|
hash_algo = self._hash.parseHashUsedElt(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)
|
|
|
|
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.getDestDir(
|
|
client, session["peer_jid"], content_data, file_data, stream_object=True
|
|
)
|
|
if confirmed:
|
|
args = [client, session, content_name, content_data]
|
|
finished_d.addCallbacks(
|
|
self._finishedCb, self._finishedEb, args, None, args
|
|
)
|
|
return confirmed
|
|
|
|
@defer.inlineCallbacks
|
|
def jingleHandler(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.getHasher()
|
|
progress_id = self.getProgressId(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.progressError(
|
|
progress_id, C.PROGRESS_ERROR_FAILED, client.profile
|
|
)
|
|
yield 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.getHasher()
|
|
content_data["stream_object"] = stream.FileStreamObject(
|
|
self.host,
|
|
client,
|
|
file_path,
|
|
uid=self.getProgressId(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._finishedCb, self._finishedEb, args, None, args)
|
|
else:
|
|
log.warning("FIXME: unmanaged action {}".format(action))
|
|
defer.returnValue(desc_elt)
|
|
|
|
def jingleSessionInfo(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.parseHashElt(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._receiverTryTerminate(
|
|
client, session, content_name, content_data
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
def jingleTerminate(self, client, action, session, content_name, jingle_elt):
|
|
if jingle_elt.decline:
|
|
# progress is the only way to tell to frontends that session has been declined
|
|
progress_id = self.getProgressId(session, content_name)
|
|
self.host.bridge.progressError(
|
|
progress_id, C.PROGRESS_ERROR_DECLINED, client.profile
|
|
)
|
|
elif not jingle_elt.success:
|
|
progress_id = self.getProgressId(session, content_name)
|
|
first_child = jingle_elt.firstChildElement()
|
|
if first_child is not None:
|
|
reason = first_child.name
|
|
else:
|
|
reason = C.PROGRESS_ERROR_FAILED
|
|
self.host.bridge.progressError(
|
|
progress_id, reason, client.profile
|
|
)
|
|
|
|
def _sendCheckSum(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.buildSessionInfo(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.buildHashElt(hash_))
|
|
iq_elt.send()
|
|
|
|
def _receiverTryTerminate(
|
|
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.delayedContentTerminate(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.delayedContentTerminate(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 _finishedCb(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._receiverTryTerminate(
|
|
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._receiverTryTerminate,
|
|
client,
|
|
session,
|
|
content_name,
|
|
content_data,
|
|
last_try=True,
|
|
)
|
|
else:
|
|
# we are the sender, we send the checksum
|
|
self._sendCheckSum(client, session, content_name, content_data)
|
|
content_data["stream_object"].close()
|
|
|
|
def _finishedEb(self, failure, client, session, content_name, content_data):
|
|
log.warning("Error while streaming file: {}".format(failure))
|
|
content_data["stream_object"].close()
|
|
self._j.contentTerminate(
|
|
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 []
|