Mercurial > libervia-backend
diff sat/plugins/plugin_xep_0234.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_xep_0234.py@a19b2c43e719 |
children | 282d1314d574 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_xep_0234.py Mon Apr 02 19:44:50 2018 +0200 @@ -0,0 +1,633 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SAT plugin for Jingle File Transfer (XEP-0234) +# Copyright (C) 2009-2018 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 sat.core.i18n import _ +from sat.core.constants import Const as C +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core import exceptions +from wokkel import disco, iwokkel +from zope.interface import implements +from sat.tools import utils +from sat.tools import stream +import os.path +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 collections import namedtuple +from sat.tools.common import regex +import mimetypes + + +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 = {u'path', u'namespace', u'file_desc', u'file_hash'} +Range = namedtuple('Range', ('offset', 'length')) + + +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 dependencies will be unloaded at the end) + Range = Range # we copy the class here, so it can be used by other plugins + + 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(NS_JINGLE_FT, self.fileJingleSend, priority = 10000, method_name=u"Jingle") + self._hash = self.host.plugins["XEP-0300"] + host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='ssssa{ss}s', out_sign='', method=self._fileJingleSend, 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 u'{}_{}'.format(session['id'], content_name) + + # generic methods + + def buildFileElement(self, name, 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, u'file')) + for name, value in ((u'name', name), (u'size', size), ('media-type', mime_type), + (u'desc', desc), (u'path', path), (u'namespace', namespace)): + if value is not None: + file_elt.addElement(name, content=unicode(value)) + + if modified is not None: + if isinstance(modified, int): + file_elt.addElement(u'date', utils.xmpp_date(modified or None)) + else: + file_elt.addElement(u'date', modified) + elif 'created' in kwargs: + file_elt.addElement(u'date', utils.xmpp_date(kwargs.pop('created'))) + + range_elt = file_elt.addElement(u'range') + if transfer_range is not None: + if transfer_range.offset is not None: + range_elt[u'offset'] = transfer_range.offset + if transfer_range.length is not None: + range_elt[u'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(u'XEP-0234_buildFileElement', file_elt, extra_args=kwargs) + if kwargs: + for kw in kwargs: + log.debug('ignored keyword: {}'.format(kw)) + return file_elt + + def buildFileElementFromDict(self, 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) + return self. buildFileElement(**file_data) + + + def parseFileElement(self, 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 are 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(u'file_elt must be None if parent_elt is set') + try: + file_elt = next(parent_elt.elements(NS_JINGLE_FT, u'file')) + except StopIteration: + raise exceptions.NotFound() + else: + if not file_elt or file_elt.uri != NS_JINGLE_FT: + raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml())) + + if file_data is None: + file_data = {} + + for name in (u'name', u'desc', u'path', u'namespace'): + try: + file_data[name] = unicode(next(file_elt.elements(NS_JINGLE_FT, name))) + except StopIteration: + pass + + + name = file_data.get(u'name') + if name == u'..': + # we don't want to go to parent dir when joining to a path + name = u'--' + file_data[u'name'] = name + elif name is not None and u'/' in name or u'\\' in name: + file_data[u'name'] = regex.pathEscape(name) + + try: + file_data[u'mime_type'] = unicode(next(file_elt.elements(NS_JINGLE_FT, u'media-type'))) + except StopIteration: + pass + + try: + file_data[u'size'] = int(unicode(next(file_elt.elements(NS_JINGLE_FT, u'size')))) + except StopIteration: + pass + + try: + file_data[u'modified'] = utils.date_parse(next(file_elt.elements(NS_JINGLE_FT, u'date'))) + except StopIteration: + pass + + try: + range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next() + except StopIteration: + pass + else: + offset = range_elt.getAttribute('offset') + length = range_elt.getAttribute('length') + if offset or length or keep_empty_range: + file_data[u'transfer_range'] = Range(offset=offset, length=length) + + prefix = u'given_' if given else u'' + hash_algo_key, hash_key = u'hash_algo', prefix + u'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(u'XEP-0234_parseFileElement', file_elt, file_data) + + return file_data + + # bridge methods + + def _fileJingleSend(self, peer_jid, filepath, name="", file_desc="", extra=None, profile=C.PROF_KEY_NONE): + client = self.host.getClient(profile) + return self.fileJingleSend(client, jid.JID(peer_jid), filepath, name or None, file_desc or None, extra or None) + + @defer.inlineCallbacks + def fileJingleSend(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 + yield 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}, + }]) + progress_id = yield progress_id_d + defer.returnValue(progress_id) + + 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 of the file + @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(_(u"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(_(u"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 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(_(u"only the following keys are allowed in extra: {keys}").format( + keys=u', '.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 = domish.Element((NS_JINGLE_FT, 'description')) + file_elt = desc_elt.addElement("file") + + if content_data[u'senders'] == self._j.ROLE_INITIATOR: + # we send a file + if name is None: + name = os.path.basename(filepath) + file_data[u'date'] = utils.xmpp_date() + file_data[u'desc'] = extra.pop(u'file_desc', u'') + file_data[u'name'] = name + mime_type = mimetypes.guess_type(name, strict=False)[0] + if mime_type is not None: + file_data[u'mime_type'] = mime_type + file_data[u'size'] = os.path.getsize(filepath) + if u'namespace' in extra: + file_data[u'namespace'] = extra[u'namespace'] + if u'path' in extra: + file_data[u'path'] = extra[u'path'] + self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'') + else: + # we request a file + file_hash = extra.pop(u'file_hash', u'') + if not name and not file_hash: + raise ValueError(_(u'you need to provide at least name or file hash')) + if name: + file_data[u'name'] = name + if file_hash: + file_data[u'file_hash'] = file_hash + file_data[u'hash_algo'] = extra[u'hash_algo'] + else: + file_data[u'hash_algo'] = self._hash.getDefaultAlgo() + if u'namespace' in extra: + file_data[u'namespace'] = extra[u'namespace'] + if u'path' in extra: + file_data[u'path'] = extra[u'path'] + self.buildFileElementFromDict(file_data, file_elt=file_elt) + + return desc_elt + + 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[u'senders'] + if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER): + log.warning(u"Bad sender, assuming initiator") + senders = content_data[u'senders'] = self._j.ROLE_INITIATOR + # first we grab file informations + try: + file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() + 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 self._fileSendingRequestConf(client, session, content_data, content_name, file_data, file_elt) + else: + # we receive the file + return self._fileReceivingRequestConf(client, session, content_data, content_name, file_data, file_elt) + + @defer.inlineCallbacks + def _fileSendingRequestConf(self, client, session, content_data, content_name, file_data, file_elt): + """parse file_elt, and handle file retrieving/permission checking""" + self.parseFileElement(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 = yield confirmed_d + if confirmed: + args = [client, session, content_name, content_data] + finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) + defer.returnValue(confirmed) + + log.warning(_(u'File continue is not implemented yet')) + defer.returnValue(False) + + def _fileReceivingRequestConf(self, client, session, content_data, content_name, file_data, file_elt): + """parse file_elt, and handle user permission/file opening""" + self.parseFileElement(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(u"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 + def gotConfirmation(confirmed): + if confirmed: + args = [client, session, content_name, content_data] + finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) + return confirmed + + # deferred to track end of transfer + finished_d = content_data['finished_d'] = defer.Deferred() + d = self._f.getDestDir(client, session['peer_jid'], content_data, file_data, stream_object=True) + d.addCallback(gotConfirmation) + return d + + 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 = desc_elt.elements(NS_JINGLE_FT, 'file').next() + try: + file_elt.elements(NS_JINGLE_FT, 'range').next() + 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[u'senders'] + if senders != session[u'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, u'file')) + size_elt = next(file_elt.elements(NS_JINGLE_FT, u'size')) + size = int(unicode(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() + content_data['stream_object'] = stream.FileStreamObject( + self.host, + client, + file_path, + mode='wb', + uid=self.getProgressId(session, content_name), + size=size, + data_cb=lambda data: hasher.update(data), + ) + 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(u"FIXME: unmanaged action {}".format(action)) + return 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(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['given_file_hash'] = 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}) [{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) + + 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(u"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(u"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(u"Hash checked, file was successfully transfered: {}".format(hash_)) + progress_metadata = {'hash': hash_, + 'hash_algo': file_data['hash_algo'], + 'hash_verified': C.BOOL_TRUE + } + error = None + else: + log.warning(u"Hash mismatch, the file was not transfered correctly") + progress_metadata=None + error = u"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, dummy, client, session, content_name, content_data): + log.info(u"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(u"Error while streaming file: {}".format(failure)) + content_data['stream_object'].close() + self._j.contentTerminate(client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT) + + +class XEP_0234_handler(XMPPHandler): + implements(iwokkel.IDisco) + + def getDiscoInfo(self, requestor, target, nodeIdentifier=''): + return [disco.DiscoFeature(NS_JINGLE_FT)] + + def getDiscoItems(self, requestor, target, nodeIdentifier=''): + return []