Mercurial > libervia-backend
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 [] |