comparison 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
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Jingle File Transfer (XEP-0234)
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.core import exceptions
25 from wokkel import disco, iwokkel
26 from zope.interface import implements
27 from sat.tools import utils
28 from sat.tools import stream
29 import os.path
30 from twisted.words.xish import domish
31 from twisted.words.protocols.jabber import jid
32 from twisted.python import failure
33 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
34 from twisted.internet import defer
35 from twisted.internet import reactor
36 from twisted.internet import error as internet_error
37 from collections import namedtuple
38 from sat.tools.common import regex
39 import mimetypes
40
41
42 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5'
43
44 PLUGIN_INFO = {
45 C.PI_NAME: "Jingle File Transfer",
46 C.PI_IMPORT_NAME: "XEP-0234",
47 C.PI_TYPE: "XEP",
48 C.PI_MODES: C.PLUG_MODE_BOTH,
49 C.PI_PROTOCOLS: ["XEP-0234"],
50 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0300", "FILE"],
51 C.PI_MAIN: "XEP_0234",
52 C.PI_HANDLER: "yes",
53 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer""")
54 }
55
56 EXTRA_ALLOWED = {u'path', u'namespace', u'file_desc', u'file_hash'}
57 Range = namedtuple('Range', ('offset', 'length'))
58
59
60 class XEP_0234(object):
61 # TODO: assure everything is closed when file is sent or session terminate is received
62 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end)
63 Range = Range # we copy the class here, so it can be used by other plugins
64
65 def __init__(self, host):
66 log.info(_("plugin Jingle File Transfer initialization"))
67 self.host = host
68 host.registerNamespace('jingle-ft', NS_JINGLE_FT)
69 self._j = host.plugins["XEP-0166"] # shortcut to access jingle
70 self._j.registerApplication(NS_JINGLE_FT, self)
71 self._f = host.plugins["FILE"]
72 self._f.register(NS_JINGLE_FT, self.fileJingleSend, priority = 10000, method_name=u"Jingle")
73 self._hash = self.host.plugins["XEP-0300"]
74 host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='ssssa{ss}s', out_sign='', method=self._fileJingleSend, async=True)
75 host.bridge.addMethod("fileJingleRequest", ".plugin", in_sign='sssssa{ss}s', out_sign='s', method=self._fileJingleRequest, async=True)
76
77 def getHandler(self, client):
78 return XEP_0234_handler()
79
80 def getProgressId(self, session, content_name):
81 """Return a unique progress ID
82
83 @param session(dict): jingle session
84 @param content_name(unicode): name of the content
85 @return (unicode): unique progress id
86 """
87 return u'{}_{}'.format(session['id'], content_name)
88
89 # generic methods
90
91 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, mime_type=None, desc=None,
92 modified=None, transfer_range=None, path=None, namespace=None, file_elt=None, **kwargs):
93 """Generate a <file> element with available metadata
94
95 @param file_hash(unicode, None): hash of the file
96 empty string to set <hash-used/> element
97 @param hash_algo(unicode, None): hash algorithm used
98 if file_hash is None and hash_algo is set, a <hash-used/> element will be generated
99 @param transfer_range(Range, None): where transfer must start/stop
100 @param modified(int, unicode, None): date of last modification
101 0 to use current date
102 int to use an unix timestamp
103 else must be an unicode string which will be used as it (it must be an XMPP time)
104 @param file_elt(domish.Element, None): element to use
105 None to create a new one
106 @param **kwargs: data for plugin extension (ignored by default)
107 @return (domish.Element): generated element
108 @trigger XEP-0234_buildFileElement(file_elt, extra_args): can be used to extend elements to add
109 """
110 if file_elt is None:
111 file_elt = domish.Element((NS_JINGLE_FT, u'file'))
112 for name, value in ((u'name', name), (u'size', size), ('media-type', mime_type),
113 (u'desc', desc), (u'path', path), (u'namespace', namespace)):
114 if value is not None:
115 file_elt.addElement(name, content=unicode(value))
116
117 if modified is not None:
118 if isinstance(modified, int):
119 file_elt.addElement(u'date', utils.xmpp_date(modified or None))
120 else:
121 file_elt.addElement(u'date', modified)
122 elif 'created' in kwargs:
123 file_elt.addElement(u'date', utils.xmpp_date(kwargs.pop('created')))
124
125 range_elt = file_elt.addElement(u'range')
126 if transfer_range is not None:
127 if transfer_range.offset is not None:
128 range_elt[u'offset'] = transfer_range.offset
129 if transfer_range.length is not None:
130 range_elt[u'length'] = transfer_range.length
131 if file_hash is not None:
132 if not file_hash:
133 file_elt.addChild(self._hash.buildHashUsedElt())
134 else:
135 file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo))
136 elif hash_algo is not None:
137 file_elt.addChild(self._hash.buildHashUsedElt(hash_algo))
138 self.host.trigger.point(u'XEP-0234_buildFileElement', file_elt, extra_args=kwargs)
139 if kwargs:
140 for kw in kwargs:
141 log.debug('ignored keyword: {}'.format(kw))
142 return file_elt
143
144 def buildFileElementFromDict(self, file_data, **kwargs):
145 """like buildFileElement but get values from a file_data dict
146
147 @param file_data(dict): metadata to use
148 @param **kwargs: data to override
149 """
150 if kwargs:
151 file_data = file_data.copy()
152 file_data.update(kwargs)
153 return self. buildFileElement(**file_data)
154
155
156 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None, keep_empty_range=False):
157 """Parse a <file> element and file dictionary accordingly
158
159 @param file_data(dict, None): dict where the data will be set
160 following keys will be set (and overwritten if they already exist):
161 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range
162 if None, a new dict is created
163 @param given(bool): if True, prefix hash key with "given_"
164 @param parent_elt(domish.Element, None): parent of the file element
165 if set, file_elt must not be set
166 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None)
167 empty range are useful to know if a peer_jid can handle range
168 @return (dict): file_data
169 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements
170 @raise exceptions.NotFound: there is not <file> element in parent_elt
171 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
172 """
173 if parent_elt is not None:
174 if file_elt is not None:
175 raise exceptions.InternalError(u'file_elt must be None if parent_elt is set')
176 try:
177 file_elt = next(parent_elt.elements(NS_JINGLE_FT, u'file'))
178 except StopIteration:
179 raise exceptions.NotFound()
180 else:
181 if not file_elt or file_elt.uri != NS_JINGLE_FT:
182 raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml()))
183
184 if file_data is None:
185 file_data = {}
186
187 for name in (u'name', u'desc', u'path', u'namespace'):
188 try:
189 file_data[name] = unicode(next(file_elt.elements(NS_JINGLE_FT, name)))
190 except StopIteration:
191 pass
192
193
194 name = file_data.get(u'name')
195 if name == u'..':
196 # we don't want to go to parent dir when joining to a path
197 name = u'--'
198 file_data[u'name'] = name
199 elif name is not None and u'/' in name or u'\\' in name:
200 file_data[u'name'] = regex.pathEscape(name)
201
202 try:
203 file_data[u'mime_type'] = unicode(next(file_elt.elements(NS_JINGLE_FT, u'media-type')))
204 except StopIteration:
205 pass
206
207 try:
208 file_data[u'size'] = int(unicode(next(file_elt.elements(NS_JINGLE_FT, u'size'))))
209 except StopIteration:
210 pass
211
212 try:
213 file_data[u'modified'] = utils.date_parse(next(file_elt.elements(NS_JINGLE_FT, u'date')))
214 except StopIteration:
215 pass
216
217 try:
218 range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next()
219 except StopIteration:
220 pass
221 else:
222 offset = range_elt.getAttribute('offset')
223 length = range_elt.getAttribute('length')
224 if offset or length or keep_empty_range:
225 file_data[u'transfer_range'] = Range(offset=offset, length=length)
226
227 prefix = u'given_' if given else u''
228 hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash'
229 try:
230 file_data[hash_algo_key], file_data[hash_key] = self._hash.parseHashElt(file_elt)
231 except exceptions.NotFound:
232 pass
233
234 self.host.trigger.point(u'XEP-0234_parseFileElement', file_elt, file_data)
235
236 return file_data
237
238 # bridge methods
239
240 def _fileJingleSend(self, peer_jid, filepath, name="", file_desc="", extra=None, profile=C.PROF_KEY_NONE):
241 client = self.host.getClient(profile)
242 return self.fileJingleSend(client, jid.JID(peer_jid), filepath, name or None, file_desc or None, extra or None)
243
244 @defer.inlineCallbacks
245 def fileJingleSend(self, client, peer_jid, filepath, name, file_desc=None, extra=None):
246 """Send a file using jingle file transfer
247
248 @param peer_jid(jid.JID): destinee jid
249 @param filepath(str): absolute path of the file
250 @param name(unicode, None): name of the file
251 @param file_desc(unicode, None): description of the file
252 @return (D(unicode)): progress id
253 """
254 progress_id_d = defer.Deferred()
255 if extra is None:
256 extra = {}
257 if file_desc is not None:
258 extra['file_desc'] = file_desc
259 yield self._j.initiate(client,
260 peer_jid,
261 [{'app_ns': NS_JINGLE_FT,
262 'senders': self._j.ROLE_INITIATOR,
263 'app_kwargs': {'filepath': filepath,
264 'name': name,
265 'extra': extra,
266 'progress_id_d': progress_id_d},
267 }])
268 progress_id = yield progress_id_d
269 defer.returnValue(progress_id)
270
271 def _fileJingleRequest(self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, profile=C.PROF_KEY_NONE):
272 client = self.host.getClient(profile)
273 return self.fileJingleRequest(client, jid.JID(peer_jid), filepath, name or None, file_hash or None, hash_algo or None, extra or None)
274
275 @defer.inlineCallbacks
276 def fileJingleRequest(self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None, extra=None):
277 """Request a file using jingle file transfer
278
279 @param peer_jid(jid.JID): destinee jid
280 @param filepath(str): absolute path of the file
281 @param name(unicode, None): name of the file
282 @param file_hash(unicode, None): hash of the file
283 @return (D(unicode)): progress id
284 """
285 progress_id_d = defer.Deferred()
286 if extra is None:
287 extra = {}
288 if file_hash is not None:
289 if hash_algo is None:
290 raise ValueError(_(u"hash_algo must be set if file_hash is set"))
291 extra['file_hash'] = file_hash
292 extra['hash_algo'] = hash_algo
293 else:
294 if hash_algo is not None:
295 raise ValueError(_(u"file_hash must be set if hash_algo is set"))
296 yield self._j.initiate(client,
297 peer_jid,
298 [{'app_ns': NS_JINGLE_FT,
299 'senders': self._j.ROLE_RESPONDER,
300 'app_kwargs': {'filepath': filepath,
301 'name': name,
302 'extra': extra,
303 'progress_id_d': progress_id_d},
304 }])
305 progress_id = yield progress_id_d
306 defer.returnValue(progress_id)
307
308 # jingle callbacks
309
310 def jingleSessionInit(self, client, session, content_name, filepath, name, extra, progress_id_d):
311 if extra is None:
312 extra = {}
313 else:
314 if not EXTRA_ALLOWED.issuperset(extra):
315 raise ValueError(_(u"only the following keys are allowed in extra: {keys}").format(
316 keys=u', '.join(EXTRA_ALLOWED)))
317 progress_id_d.callback(self.getProgressId(session, content_name))
318 content_data = session['contents'][content_name]
319 application_data = content_data['application_data']
320 assert 'file_path' not in application_data
321 application_data['file_path'] = filepath
322 file_data = application_data['file_data'] = {}
323 desc_elt = domish.Element((NS_JINGLE_FT, 'description'))
324 file_elt = desc_elt.addElement("file")
325
326 if content_data[u'senders'] == self._j.ROLE_INITIATOR:
327 # we send a file
328 if name is None:
329 name = os.path.basename(filepath)
330 file_data[u'date'] = utils.xmpp_date()
331 file_data[u'desc'] = extra.pop(u'file_desc', u'')
332 file_data[u'name'] = name
333 mime_type = mimetypes.guess_type(name, strict=False)[0]
334 if mime_type is not None:
335 file_data[u'mime_type'] = mime_type
336 file_data[u'size'] = os.path.getsize(filepath)
337 if u'namespace' in extra:
338 file_data[u'namespace'] = extra[u'namespace']
339 if u'path' in extra:
340 file_data[u'path'] = extra[u'path']
341 self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'')
342 else:
343 # we request a file
344 file_hash = extra.pop(u'file_hash', u'')
345 if not name and not file_hash:
346 raise ValueError(_(u'you need to provide at least name or file hash'))
347 if name:
348 file_data[u'name'] = name
349 if file_hash:
350 file_data[u'file_hash'] = file_hash
351 file_data[u'hash_algo'] = extra[u'hash_algo']
352 else:
353 file_data[u'hash_algo'] = self._hash.getDefaultAlgo()
354 if u'namespace' in extra:
355 file_data[u'namespace'] = extra[u'namespace']
356 if u'path' in extra:
357 file_data[u'path'] = extra[u'path']
358 self.buildFileElementFromDict(file_data, file_elt=file_elt)
359
360 return desc_elt
361
362 def jingleRequestConfirmation(self, client, action, session, content_name, desc_elt):
363 """This method request confirmation for a jingle session"""
364 content_data = session['contents'][content_name]
365 senders = content_data[u'senders']
366 if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER):
367 log.warning(u"Bad sender, assuming initiator")
368 senders = content_data[u'senders'] = self._j.ROLE_INITIATOR
369 # first we grab file informations
370 try:
371 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next()
372 except StopIteration:
373 raise failure.Failure(exceptions.DataError)
374 file_data = {'progress_id': self.getProgressId(session, content_name)}
375
376 if senders == self._j.ROLE_RESPONDER:
377 # we send the file
378 return self._fileSendingRequestConf(client, session, content_data, content_name, file_data, file_elt)
379 else:
380 # we receive the file
381 return self._fileReceivingRequestConf(client, session, content_data, content_name, file_data, file_elt)
382
383 @defer.inlineCallbacks
384 def _fileSendingRequestConf(self, client, session, content_data, content_name, file_data, file_elt):
385 """parse file_elt, and handle file retrieving/permission checking"""
386 self.parseFileElement(file_elt, file_data)
387 content_data['application_data']['file_data'] = file_data
388 finished_d = content_data['finished_d'] = defer.Deferred()
389
390 # confirmed_d is a deferred returning confimed value (only used if cont is False)
391 cont, confirmed_d = self.host.trigger.returnPoint("XEP-0234_fileSendingRequest", client, session, content_data, content_name, file_data, file_elt)
392 if not cont:
393 confirmed = yield confirmed_d
394 if confirmed:
395 args = [client, session, content_name, content_data]
396 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
397 defer.returnValue(confirmed)
398
399 log.warning(_(u'File continue is not implemented yet'))
400 defer.returnValue(False)
401
402 def _fileReceivingRequestConf(self, client, session, content_data, content_name, file_data, file_elt):
403 """parse file_elt, and handle user permission/file opening"""
404 self.parseFileElement(file_elt, file_data, given=True)
405 try:
406 hash_algo, file_data['given_file_hash'] = self._hash.parseHashElt(file_elt)
407 except exceptions.NotFound:
408 try:
409 hash_algo = self._hash.parseHashUsedElt(file_elt)
410 except exceptions.NotFound:
411 raise failure.Failure(exceptions.DataError)
412
413 if hash_algo is not None:
414 file_data['hash_algo'] = hash_algo
415 file_data['hash_hasher'] = hasher = self._hash.getHasher(hash_algo)
416 file_data['data_cb'] = lambda data: hasher.update(data)
417
418 try:
419 file_data['size'] = int(file_data['size'])
420 except ValueError:
421 raise failure.Failure(exceptions.DataError)
422
423 name = file_data['name']
424 if '/' in name or '\\' in name:
425 log.warning(u"File name contain path characters, we replace them: {}".format(name))
426 file_data['name'] = name.replace('/', '_').replace('\\', '_')
427
428 content_data['application_data']['file_data'] = file_data
429
430 # now we actualy request permission to user
431 def gotConfirmation(confirmed):
432 if confirmed:
433 args = [client, session, content_name, content_data]
434 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
435 return confirmed
436
437 # deferred to track end of transfer
438 finished_d = content_data['finished_d'] = defer.Deferred()
439 d = self._f.getDestDir(client, session['peer_jid'], content_data, file_data, stream_object=True)
440 d.addCallback(gotConfirmation)
441 return d
442
443 def jingleHandler(self, client, action, session, content_name, desc_elt):
444 content_data = session['contents'][content_name]
445 application_data = content_data['application_data']
446 if action in (self._j.A_ACCEPTED_ACK,):
447 pass
448 elif action == self._j.A_SESSION_INITIATE:
449 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next()
450 try:
451 file_elt.elements(NS_JINGLE_FT, 'range').next()
452 except StopIteration:
453 # initiator doesn't manage <range>, but we do so we advertise it
454 # FIXME: to be checked
455 log.debug("adding <range> element")
456 file_elt.addElement('range')
457 elif action == self._j.A_SESSION_ACCEPT:
458 assert not 'stream_object' in content_data
459 file_data = application_data['file_data']
460 file_path = application_data['file_path']
461 senders = content_data[u'senders']
462 if senders != session[u'role']:
463 # we are receiving the file
464 try:
465 # did the responder specified the size of the file?
466 file_elt = next(desc_elt.elements(NS_JINGLE_FT, u'file'))
467 size_elt = next(file_elt.elements(NS_JINGLE_FT, u'size'))
468 size = int(unicode(size_elt))
469 except (StopIteration, ValueError):
470 size = None
471 # XXX: hash security is not critical here, so we just take the higher mandatory one
472 hasher = file_data['hash_hasher'] = self._hash.getHasher()
473 content_data['stream_object'] = stream.FileStreamObject(
474 self.host,
475 client,
476 file_path,
477 mode='wb',
478 uid=self.getProgressId(session, content_name),
479 size=size,
480 data_cb=lambda data: hasher.update(data),
481 )
482 else:
483 # we are sending the file
484 size = file_data['size']
485 # XXX: hash security is not critical here, so we just take the higher mandatory one
486 hasher = file_data['hash_hasher'] = self._hash.getHasher()
487 content_data['stream_object'] = stream.FileStreamObject(
488 self.host,
489 client,
490 file_path,
491 uid=self.getProgressId(session, content_name),
492 size=size,
493 data_cb=lambda data: hasher.update(data),
494 )
495 finished_d = content_data['finished_d'] = defer.Deferred()
496 args = [client, session, content_name, content_data]
497 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
498 else:
499 log.warning(u"FIXME: unmanaged action {}".format(action))
500 return desc_elt
501
502 def jingleSessionInfo(self, client, action, session, content_name, jingle_elt):
503 """Called on session-info action
504
505 manage checksum, and ignore <received/> element
506 """
507 # TODO: manage <received/> element
508 content_data = session['contents'][content_name]
509 elts = [elt for elt in jingle_elt.elements() if elt.uri == NS_JINGLE_FT]
510 if not elts:
511 return
512 for elt in elts:
513 if elt.name == 'received':
514 pass
515 elif elt.name == 'checksum':
516 # we have received the file hash, we need to parse it
517 if content_data['senders'] == session['role']:
518 log.warning(u"unexpected checksum received while we are the file sender")
519 raise exceptions.DataError
520 info_content_name = elt['name']
521 if info_content_name != content_name:
522 # it was for an other content...
523 return
524 file_data = content_data['application_data']['file_data']
525 try:
526 file_elt = elt.elements((NS_JINGLE_FT, 'file')).next()
527 except StopIteration:
528 raise exceptions.DataError
529 algo, file_data['given_file_hash'] = self._hash.parseHashElt(file_elt)
530 if algo != file_data.get('hash_algo'):
531 log.warning(u"Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo}) [{profile}]"
532 .format(peer_algo=algo, our_algo=file_data.get('hash_algo'), profile=client.profile))
533 else:
534 self._receiverTryTerminate(client, session, content_name, content_data)
535 else:
536 raise NotImplementedError
537
538 def jingleTerminate(self, client, action, session, content_name, jingle_elt):
539 if jingle_elt.decline:
540 # progress is the only way to tell to frontends that session has been declined
541 progress_id = self.getProgressId(session, content_name)
542 self.host.bridge.progressError(progress_id, C.PROGRESS_ERROR_DECLINED, client.profile)
543
544 def _sendCheckSum(self, client, session, content_name, content_data):
545 """Send the session-info with the hash checksum"""
546 file_data = content_data['application_data']['file_data']
547 hasher = file_data['hash_hasher']
548 hash_ = hasher.hexdigest()
549 log.debug(u"Calculated hash: {}".format(hash_))
550 iq_elt, jingle_elt = self._j.buildSessionInfo(client, session)
551 checksum_elt = jingle_elt.addElement((NS_JINGLE_FT, 'checksum'))
552 checksum_elt['creator'] = content_data['creator']
553 checksum_elt['name'] = content_name
554 file_elt = checksum_elt.addElement('file')
555 file_elt.addChild(self._hash.buildHashElt(hash_))
556 iq_elt.send()
557
558 def _receiverTryTerminate(self, client, session, content_name, content_data, last_try=False):
559 """Try to terminate the session
560
561 This method must only be used by the receiver.
562 It check if transfer is finished, and hash available,
563 if everything is OK, it check hash and terminate the session
564 @param last_try(bool): if True this mean than session must be terminated even given hash is not available
565 @return (bool): True if session was terminated
566 """
567 if not content_data.get('transfer_finished', False):
568 return False
569 file_data = content_data['application_data']['file_data']
570 given_hash = file_data.get('given_file_hash')
571 if given_hash is None:
572 if last_try:
573 log.warning(u"sender didn't sent hash checksum, we can't check the file [{profile}]".format(profile=client.profile))
574 self._j.delayedContentTerminate(client, session, content_name)
575 content_data['stream_object'].close()
576 return True
577 return False
578 hasher = file_data['hash_hasher']
579 hash_ = hasher.hexdigest()
580
581 if hash_ == given_hash:
582 log.info(u"Hash checked, file was successfully transfered: {}".format(hash_))
583 progress_metadata = {'hash': hash_,
584 'hash_algo': file_data['hash_algo'],
585 'hash_verified': C.BOOL_TRUE
586 }
587 error = None
588 else:
589 log.warning(u"Hash mismatch, the file was not transfered correctly")
590 progress_metadata=None
591 error = u"Hash mismatch: given={algo}:{given}, calculated={algo}:{our}".format(
592 algo = file_data['hash_algo'],
593 given = given_hash,
594 our = hash_)
595
596 self._j.delayedContentTerminate(client, session, content_name)
597 content_data['stream_object'].close(progress_metadata, error)
598 # we may have the last_try timer still active, so we try to cancel it
599 try:
600 content_data['last_try_timer'].cancel()
601 except (KeyError, internet_error.AlreadyCalled):
602 pass
603 return True
604
605 def _finishedCb(self, dummy, client, session, content_name, content_data):
606 log.info(u"File transfer terminated")
607 if content_data['senders'] != session['role']:
608 # we terminate the session only if we are the receiver,
609 # as recommanded in XEP-0234 §2 (after example 6)
610 content_data['transfer_finished'] = True
611 if not self._receiverTryTerminate(client, session, content_name, content_data):
612 # we have not received the hash yet, we wait 5 more seconds
613 content_data['last_try_timer'] = reactor.callLater(
614 5, self._receiverTryTerminate, client, session, content_name, content_data, last_try=True)
615 else:
616 # we are the sender, we send the checksum
617 self._sendCheckSum(client, session, content_name, content_data)
618 content_data['stream_object'].close()
619
620 def _finishedEb(self, failure, client, session, content_name, content_data):
621 log.warning(u"Error while streaming file: {}".format(failure))
622 content_data['stream_object'].close()
623 self._j.contentTerminate(client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT)
624
625
626 class XEP_0234_handler(XMPPHandler):
627 implements(iwokkel.IDisco)
628
629 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
630 return [disco.DiscoFeature(NS_JINGLE_FT)]
631
632 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
633 return []