comparison src/plugins/plugin_xep_0234.py @ 2502:7ad5f2c4e34a

XEP-0065,XEP-0096,XEP-0166,XEP-0235,XEP-0300: file transfer improvments: huge patch sorry :) many things are improved by this patch, notably: - updated to last protocol changes (urn:xmpp:jingle:apps:file-transfer:5 and urn:xmpp:hashes:2) - XEP-0234: file request implementation - XEP-0234: methods to parse and generate <file> element (can be used by other plugins easily) - XEP-0234: range data is now in a namedtuple - path and namespace can be specified when sending/requesting a file (not standard, but needed for file sharing) - better error/termination handling - trigger points to handle file requests by other plugins - preparation to use file plugins with components
author Goffi <goffi@goffi.org>
date Wed, 28 Feb 2018 18:28:39 +0100
parents e2a7bb875957
children 025afb04c10b
comparison
equal deleted inserted replaced
2501:3b67fe672206 2502:7ad5f2c4e34a
32 from twisted.python import failure 32 from twisted.python import failure
33 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 33 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
34 from twisted.internet import defer 34 from twisted.internet import defer
35 from twisted.internet import reactor 35 from twisted.internet import reactor
36 from twisted.internet import error as internet_error 36 from twisted.internet import error as internet_error
37 37 from collections import namedtuple
38 38 import regex
39 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4' 39
40
41 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5'
40 42
41 PLUGIN_INFO = { 43 PLUGIN_INFO = {
42 C.PI_NAME: "Jingle File Transfer", 44 C.PI_NAME: "Jingle File Transfer",
43 C.PI_IMPORT_NAME: "XEP-0234", 45 C.PI_IMPORT_NAME: "XEP-0234",
44 C.PI_TYPE: "XEP", 46 C.PI_TYPE: "XEP",
47 C.PI_MODES: C.PLUG_MODE_BOTH,
45 C.PI_PROTOCOLS: ["XEP-0234"], 48 C.PI_PROTOCOLS: ["XEP-0234"],
46 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0300", "FILE"], 49 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0300", "FILE"],
47 C.PI_MAIN: "XEP_0234", 50 C.PI_MAIN: "XEP_0234",
48 C.PI_HANDLER: "yes", 51 C.PI_HANDLER: "yes",
49 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer""") 52 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer""")
50 } 53 }
51 54
55 EXTRA_ALLOWED = {u'path', u'namespace', u'file_desc', u'file_hash'}
56 Range = namedtuple('Range', ('offset', 'length'))
57
52 58
53 class XEP_0234(object): 59 class XEP_0234(object):
54 # TODO: assure everything is closed when file is sent or session terminate is received 60 # TODO: assure everything is closed when file is sent or session terminate is received
55 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end) 61 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end)
56 62
57 def __init__(self, host): 63 def __init__(self, host):
58 log.info(_("plugin Jingle File Transfer initialization")) 64 log.info(_("plugin Jingle File Transfer initialization"))
59 self.host = host 65 self.host = host
66 host.registerNamespace('jingle-ft', NS_JINGLE_FT)
60 self._j = host.plugins["XEP-0166"] # shortcut to access jingle 67 self._j = host.plugins["XEP-0166"] # shortcut to access jingle
61 self._j.registerApplication(NS_JINGLE_FT, self) 68 self._j.registerApplication(NS_JINGLE_FT, self)
62 self._f = host.plugins["FILE"] 69 self._f = host.plugins["FILE"]
63 self._f.register(NS_JINGLE_FT, self.fileJingleSend, priority = 10000, method_name=u"Jingle") 70 self._f.register(NS_JINGLE_FT, self.fileJingleSend, priority = 10000, method_name=u"Jingle")
64 self._hash = self.host.plugins["XEP-0300"] 71 self._hash = self.host.plugins["XEP-0300"]
65 host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='sssss', out_sign='', method=self._fileJingleSend) 72 host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='ssssa{ss}s', out_sign='', method=self._fileJingleSend, async=True)
73 host.bridge.addMethod("fileJingleRequest", ".plugin", in_sign='sssssa{ss}s', out_sign='s', method=self._fileJingleRequest, async=True)
66 74
67 def getHandler(self, client): 75 def getHandler(self, client):
68 return XEP_0234_handler() 76 return XEP_0234_handler()
69 77
70 def _getProgressId(self, session, content_name): 78 def getProgressId(self, session, content_name):
71 """Return a unique progress ID 79 """Return a unique progress ID
72 80
73 @param session(dict): jingle session 81 @param session(dict): jingle session
74 @param content_name(unicode): name of the content 82 @param content_name(unicode): name of the content
75 @return (unicode): unique progress id 83 @return (unicode): unique progress id
76 """ 84 """
77 return u'{}_{}'.format(session['id'], content_name) 85 return u'{}_{}'.format(session['id'], content_name)
78 86
79 def _fileJingleSend(self, peer_jid, filepath, name="", file_desc="", profile=C.PROF_KEY_NONE): 87 # generic methods
88
89 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, media_type=None, desc=None,
90 date=None, range_offset=None, range_length=None, path=None, namespace=None, file_elt=None):
91 """Generate a <file> element with available metadata
92
93 @param file_hash(unicode, None): hash of the file
94 empty string to set <hash-used/> element
95 @param hash_algo(unicode, None): hash algorithm used
96 if file_hash is None and hash_algo is set, a <hash-used/> element will be generated
97 @param range_offset(int, None): offset where transfer must start
98 use -1 to add an empty <range/> element
99 @param date(int, unicode, None): date of last modification
100 0 to use current date
101 int to use an unix timestamp
102 else must be an unicode string which will be used as it (it must be an XMPP time)
103 @param file_elt(domish.Element, None): element to use
104 None to create a new one
105 @return (domish.Element): generated element
106 """
107 if file_elt is None:
108 file_elt = domish.Element((NS_JINGLE_FT, u'file'))
109 for name, value in ((u'name', name), (u'size', size), ('media-type', media_type),
110 (u'desc', desc), (u'path', path), (u'namespace', namespace)):
111 if value is not None:
112 file_elt.addElement(name, content=unicode(value))
113 if date is not None:
114 if isinstance(date, int):
115 file_elt.addElement(u'date', utils.xmpp_date(date or None))
116 else:
117 file_elt.addElement(u'date', date)
118 if range_offset or range_length:
119 range_elt = file_elt.addElement(u'range')
120 if range_offset is not None and range_offset != -1:
121 range_elt[u'offset'] = range_offset
122 if range_length is not None:
123 range_elt[u'length'] = range_length
124 if file_hash is not None:
125 if not file_hash:
126 file_elt.addChild(self._hash.buildHashUsedElt())
127 else:
128 file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo))
129 elif hash_algo is not None:
130 file_elt.addChild(self._hash.buildHashUsedElt(hash_algo))
131 return file_elt
132
133 def buildFileElementFromDict(self, file_data, file_elt = None, **kwargs):
134 """like buildFileElement but get values from a file_data dict
135
136 @param file_data(dict): metadata to use
137 @param **kwargs: data to override
138 """
139 if kwargs:
140 file_data = file_data.copy()
141 file_data.update(kwargs)
142 (name, file_hash, hash_algo, size, media_type,
143 desc, date, path, namespace) = (file_data.get(u'name'),
144 file_data.get(u'file_hash'),
145 file_data.get(u'hash_algo'),
146 file_data.get(u'size'),
147 file_data.get(u'media-type'),
148 file_data.get(u'desc'),
149 file_data.get(u'date'),
150 file_data.get(u'path'),
151 file_data.get(u'namespace'))
152 try:
153 range_offset, range_length = file_data[u'range']
154 except KeyError:
155 range_offset = range_length = None
156 return self. buildFileElement(name = name, file_hash = file_hash, hash_algo = hash_algo, size = size,
157 media_type = media_type, desc = desc, date = date,
158 range_offset = range_offset, range_length = range_length, path = path,
159 namespace = namespace, file_elt = file_elt)
160
161
162 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None):
163 """Parse a <file> element and file dictionary accordingly
164
165 @param file_data(dict, None): dict where the data will be set
166 following keys will be set (and overwritten if they already exist):
167 name, file_hash, hash_algo, size, media-type, desc, path, namespace, range
168 if None, a new dict is created
169 @param given(bool): if True, prefix hash key with "given_"
170 @param parent_elt(domish.Element, None): parent of the file element
171 if set, file_elt must not be set
172 @return (dict): file_data
173 @raise exceptions.NotFound: there is not <file> element in parent_elt
174 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
175 """
176 if parent_elt is not None:
177 if file_elt is not None:
178 raise exceptions.InternalError(u'file_elt must be None if parent_elt is set')
179 try:
180 file_elt = next(parent_elt.elements(NS_JINGLE_FT, u'file'))
181 except StopIteration:
182 raise exceptions.NotFound()
183 else:
184 if not file_elt or file_elt.uri != NS_JINGLE_FT:
185 raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml()))
186
187 if file_data is None:
188 file_data = {}
189
190 for name in (u'name', u'media-type', u'desc', u'path', u'namespace'):
191 try:
192 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next())
193 except StopIteration:
194 pass
195
196 name = file_data.get(u'name')
197 if name == u'..':
198 # we don't want to go to parent dir when joining to a path
199 name = u'--'
200 file_data[u'name'] = name
201 elif name is not None and u'/' in name or u'\\' in name:
202 file_data[u'name'] = regex.pathEscape(name)
203
204 try:
205 file_data[u'size'] = int(unicode(file_elt.elements(NS_JINGLE_FT, u'size').next()))
206 except StopIteration:
207 pass
208
209 try:
210 range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next()
211 except StopIteration:
212 pass
213 else:
214 offset = range_elt.getAttribute('offset')
215 length = range_elt.getAttribute('length')
216 file_data[u'range'] = Range(offset=offset, length=length)
217
218 prefix = u'given_' if given else u''
219 hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash'
220 try:
221 file_data[hash_algo_key], file_data[hash_key] = self._hash.parseHashElt(file_elt)
222 except exceptions.NotFound:
223 pass
224
225 return file_data
226
227 # bridge methods
228
229 def _fileJingleSend(self, peer_jid, filepath, name="", file_desc="", extra=None, profile=C.PROF_KEY_NONE):
80 client = self.host.getClient(profile) 230 client = self.host.getClient(profile)
81 return self.fileJingleSend(client, jid.JID(peer_jid), filepath, name or None, file_desc or None) 231 return self.fileJingleSend(client, jid.JID(peer_jid), filepath, name or None, file_desc or None, extra or None)
82 232
83 def fileJingleSend(self, client, peer_jid, filepath, name, file_desc=None): 233 @defer.inlineCallbacks
234 def fileJingleSend(self, client, peer_jid, filepath, name, file_desc=None, extra=None):
84 """Send a file using jingle file transfer 235 """Send a file using jingle file transfer
85 236
86 @param peer_jid(jid.JID): destinee jid 237 @param peer_jid(jid.JID): destinee jid
87 @param filepath(str): absolute path of the file 238 @param filepath(str): absolute path of the file
88 @param name(unicode, None): name of the file 239 @param name(unicode, None): name of the file
89 @param file_desc(unicode, None): description of the file 240 @param file_desc(unicode, None): description of the file
90 @return (D(unicode)): progress id 241 @return (D(unicode)): progress id
91 """ 242 """
92 progress_id_d = defer.Deferred() 243 progress_id_d = defer.Deferred()
93 self._j.initiate(client, 244 if extra is None:
94 peer_jid, 245 extra = {}
95 [{'app_ns': NS_JINGLE_FT, 246 if file_desc is not None:
96 'senders': self._j.ROLE_INITIATOR, 247 extra['file_desc'] = file_desc
97 'app_kwargs': {'filepath': filepath, 248 yield self._j.initiate(client,
98 'name': name, 249 peer_jid,
99 'file_desc': file_desc, 250 [{'app_ns': NS_JINGLE_FT,
100 'progress_id_d': progress_id_d}, 251 'senders': self._j.ROLE_INITIATOR,
101 }]) 252 'app_kwargs': {'filepath': filepath,
102 return progress_id_d 253 'name': name,
254 'extra': extra,
255 'progress_id_d': progress_id_d},
256 }])
257 progress_id = yield progress_id_d
258 defer.returnValue(progress_id)
259
260 def _fileJingleRequest(self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, profile=C.PROF_KEY_NONE):
261 client = self.host.getClient(profile)
262 return self.fileJingleRequest(client, jid.JID(peer_jid), filepath, name or None, file_hash or None, hash_algo or None, extra or None)
263
264 @defer.inlineCallbacks
265 def fileJingleRequest(self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None, extra=None):
266 """Request a file using jingle file transfer
267
268 @param peer_jid(jid.JID): destinee jid
269 @param filepath(str): absolute path of the file
270 @param name(unicode, None): name of the file
271 @param file_hash(unicode, None): hash of the file
272 @return (D(unicode)): progress id
273 """
274 progress_id_d = defer.Deferred()
275 if extra is None:
276 extra = {}
277 if file_hash is not None:
278 if hash_algo is None:
279 raise ValueError(_(u"hash_algo must be set if file_hash is set"))
280 extra['file_hash'] = file_hash
281 extra['hash_algo'] = hash_algo
282 else:
283 if hash_algo is not None:
284 raise ValueError(_(u"file_hash must be set if hash_algo is set"))
285 yield self._j.initiate(client,
286 peer_jid,
287 [{'app_ns': NS_JINGLE_FT,
288 'senders': self._j.ROLE_RESPONDER,
289 'app_kwargs': {'filepath': filepath,
290 'name': name,
291 'extra': extra,
292 'progress_id_d': progress_id_d},
293 }])
294 progress_id = yield progress_id_d
295 defer.returnValue(progress_id)
103 296
104 # jingle callbacks 297 # jingle callbacks
105 298
106 def jingleSessionInit(self, client, session, content_name, filepath, name, file_desc, progress_id_d): 299 def jingleSessionInit(self, client, session, content_name, filepath, name, extra, progress_id_d):
107 progress_id_d.callback(self._getProgressId(session, content_name)) 300 if extra is None:
301 extra = {}
302 else:
303 if not EXTRA_ALLOWED.issuperset(extra):
304 raise ValueError(_(u"only the following keys are allowed in extra: {keys}").format(
305 keys=u', '.join(EXTRA_ALLOWED)))
306 progress_id_d.callback(self.getProgressId(session, content_name))
108 content_data = session['contents'][content_name] 307 content_data = session['contents'][content_name]
109 application_data = content_data['application_data'] 308 application_data = content_data['application_data']
110 assert 'file_path' not in application_data 309 assert 'file_path' not in application_data
111 application_data['file_path'] = filepath 310 application_data['file_path'] = filepath
112 file_data = application_data['file_data'] = {} 311 file_data = application_data['file_data'] = {}
113 file_data['date'] = utils.xmpp_date()
114 file_data['desc'] = file_desc or ''
115 file_data['media-type'] = "application/octet-stream" # TODO
116 file_data['name'] = os.path.basename(filepath) if name is None else name
117 file_data['size'] = os.path.getsize(filepath)
118 desc_elt = domish.Element((NS_JINGLE_FT, 'description')) 312 desc_elt = domish.Element((NS_JINGLE_FT, 'description'))
119 file_elt = desc_elt.addElement("file") 313 file_elt = desc_elt.addElement("file")
120 for name in ('date', 'desc', 'media-type', 'name', 'size'): 314
121 file_elt.addElement(name, content=unicode(file_data[name])) 315 if content_data[u'senders'] == self._j.ROLE_INITIATOR:
122 file_elt.addElement("range") # TODO 316 # we send a file
123 file_elt.addChild(self._hash.buildHashElt()) 317 file_data[u'date'] = utils.xmpp_date()
318 file_data[u'desc'] = extra.pop(u'file_desc', u'')
319 file_data[u'media-type'] = "application/octet-stream" # TODO
320 file_data[u'name'] = os.path.basename(filepath) if name is None else name
321 file_data[u'size'] = os.path.getsize(filepath)
322 if u'namespace' in extra:
323 file_data[u'namespace'] = extra[u'namespace']
324 if u'path' in extra:
325 file_data[u'path'] = extra[u'path']
326 self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'')
327 file_elt.addElement("range") # TODO
328 else:
329 # we request a file
330 file_hash = extra.pop(u'file_hash', u'')
331 if not name and not file_hash:
332 raise ValueError(_(u'you need to provide at least name or file hash'))
333 if name:
334 file_data[u'name'] = name
335 if file_hash:
336 file_data[u'file_hash'] = file_hash
337 file_data[u'hash_algo'] = extra[u'hash_algo']
338 else:
339 file_data[u'hash_algo'] = self._hash.getDefaultAlgo()
340 if u'namespace' in extra:
341 file_data[u'namespace'] = extra[u'namespace']
342 if u'path' in extra:
343 file_data[u'path'] = extra[u'path']
344 self.buildFileElementFromDict(file_data, file_elt=file_elt)
345
124 return desc_elt 346 return desc_elt
125 347
126 def jingleRequestConfirmation(self, client, action, session, content_name, desc_elt): 348 def jingleRequestConfirmation(self, client, action, session, content_name, desc_elt):
127 """This method request confirmation for a jingle session""" 349 """This method request confirmation for a jingle session"""
128 content_data = session['contents'][content_name] 350 content_data = session['contents'][content_name]
129 if content_data['senders'] not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER): 351 senders = content_data[u'senders']
352 if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER):
130 log.warning(u"Bad sender, assuming initiator") 353 log.warning(u"Bad sender, assuming initiator")
131 content_data['senders'] = self._j.ROLE_INITIATOR 354 senders = content_data[u'senders'] = self._j.ROLE_INITIATOR
132 # first we grab file informations 355 # first we grab file informations
133 try: 356 try:
134 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() 357 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next()
135 except StopIteration: 358 except StopIteration:
136 raise failure.Failure(exceptions.DataError) 359 raise failure.Failure(exceptions.DataError)
137 file_data = {'progress_id': self._getProgressId(session, content_name)} 360 file_data = {'progress_id': self.getProgressId(session, content_name)}
138 for name in ('date', 'desc', 'media-type', 'name', 'range', 'size'): 361
362 if senders == self._j.ROLE_RESPONDER:
363 # we send the file
364 return self._fileSendingRequestConf(client, session, content_data, content_name, file_data, file_elt)
365 else:
366 # we receive the file
367 return self._fileReceivingRequestConf(client, session, content_data, content_name, file_data, file_elt)
368
369 @defer.inlineCallbacks
370 def _fileSendingRequestConf(self, client, session, content_data, content_name, file_data, file_elt):
371 """parse file_elt, and handle file retrieving/permission checking"""
372 self.parseFileElement(file_elt, file_data)
373 content_data['application_data']['file_data'] = file_data
374 finished_d = content_data['finished_d'] = defer.Deferred()
375
376 # confirmed_d is a deferred returning confimed value (only used if cont is False)
377 cont, confirmed_d = self.host.trigger.returnPoint("XEP-0234_fileSendingRequest", client, session, content_data, content_name, file_data, file_elt)
378 if not cont:
379 confirmed = yield confirmed_d
380 if confirmed:
381 args = [client, session, content_name, content_data]
382 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
383 defer.returnValue(confirmed)
384
385 log.warning(_(u'File continue is not implemented yet'))
386 defer.returnValue(False)
387
388 def _fileReceivingRequestConf(self, client, session, content_data, content_name, file_data, file_elt):
389 """parse file_elt, and handle user permission/file opening"""
390 self.parseFileElement(file_elt, file_data, given=True)
391 try:
392 hash_algo, file_data['given_file_hash'] = self._hash.parseHashElt(file_elt)
393 except exceptions.NotFound:
139 try: 394 try:
140 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) 395 hash_algo = self._hash.parseHashUsedElt(file_elt)
141 except StopIteration: 396 except exceptions.NotFound:
142 file_data[name] = '' 397 raise failure.Failure(exceptions.DataError)
143
144 try:
145 hash_algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt)
146 except exceptions.NotFound:
147 raise failure.Failure(exceptions.DataError)
148 398
149 if hash_algo is not None: 399 if hash_algo is not None:
150 file_data['hash_algo'] = hash_algo 400 file_data['hash_algo'] = hash_algo
151 file_data['hash_hasher'] = hasher = self._hash.getHasher(hash_algo) 401 file_data['hash_hasher'] = hasher = self._hash.getHasher(hash_algo)
152 file_data['data_cb'] = lambda data: hasher.update(data) 402 file_data['data_cb'] = lambda data: hasher.update(data)
164 content_data['application_data']['file_data'] = file_data 414 content_data['application_data']['file_data'] = file_data
165 415
166 # now we actualy request permission to user 416 # now we actualy request permission to user
167 def gotConfirmation(confirmed): 417 def gotConfirmation(confirmed):
168 if confirmed: 418 if confirmed:
169 finished_d = content_data['finished_d'] = defer.Deferred()
170 args = [client, session, content_name, content_data] 419 args = [client, session, content_name, content_data]
171 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) 420 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
172 return confirmed 421 return confirmed
173 422
423 # deferred to track end of transfer
424 finished_d = content_data['finished_d'] = defer.Deferred()
174 d = self._f.getDestDir(client, session['peer_jid'], content_data, file_data, stream_object=True) 425 d = self._f.getDestDir(client, session['peer_jid'], content_data, file_data, stream_object=True)
175 d.addCallback(gotConfirmation) 426 d.addCallback(gotConfirmation)
176 return d 427 return d
177 428
178 def jingleHandler(self, client, action, session, content_name, desc_elt): 429 def jingleHandler(self, client, action, session, content_name, desc_elt):
184 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() 435 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next()
185 try: 436 try:
186 file_elt.elements(NS_JINGLE_FT, 'range').next() 437 file_elt.elements(NS_JINGLE_FT, 'range').next()
187 except StopIteration: 438 except StopIteration:
188 # initiator doesn't manage <range>, but we do so we advertise it 439 # initiator doesn't manage <range>, but we do so we advertise it
440 # FIXME: to be checked
189 log.debug("adding <range> element") 441 log.debug("adding <range> element")
190 file_elt.addElement('range') 442 file_elt.addElement('range')
191 elif action == self._j.A_SESSION_ACCEPT: 443 elif action == self._j.A_SESSION_ACCEPT:
192 assert not 'stream_object' in content_data 444 assert not 'stream_object' in content_data
193 file_data = application_data['file_data'] 445 file_data = application_data['file_data']
194 file_path = application_data['file_path'] 446 file_path = application_data['file_path']
195 size = file_data['size'] 447 senders = content_data[u'senders']
196 # XXX: hash security is not critical here, so we just take the higher mandatory one 448 if senders != session[u'role']:
197 hasher = file_data['hash_hasher'] = self._hash.getHasher('sha-256') 449 # we are receiving the file
198 content_data['stream_object'] = stream.FileStreamObject( 450 try:
199 self.host, 451 # did the responder specified the size of the file?
200 client, 452 file_elt = next(desc_elt.elements(NS_JINGLE_FT, u'file'))
201 file_path, 453 size_elt = next(file_elt.elements(NS_JINGLE_FT, u'size'))
202 uid=self._getProgressId(session, content_name), 454 size = int(unicode(size_elt))
203 size=size, 455 except (StopIteration, ValueError):
204 data_cb=lambda data: hasher.update(data), 456 size = None
205 ) 457 # XXX: hash security is not critical here, so we just take the higher mandatory one
458 hasher = file_data['hash_hasher'] = self._hash.getHasher()
459 content_data['stream_object'] = stream.FileStreamObject(
460 self.host,
461 client,
462 file_path,
463 mode='wb',
464 uid=self.getProgressId(session, content_name),
465 size=size,
466 data_cb=lambda data: hasher.update(data),
467 )
468 else:
469 # we are sending the file
470 size = file_data['size']
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 uid=self.getProgressId(session, content_name),
478 size=size,
479 data_cb=lambda data: hasher.update(data),
480 )
206 finished_d = content_data['finished_d'] = defer.Deferred() 481 finished_d = content_data['finished_d'] = defer.Deferred()
207 args = [client, session, content_name, content_data] 482 args = [client, session, content_name, content_data]
208 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) 483 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
209 else: 484 else:
210 log.warning(u"FIXME: unmanaged action {}".format(action)) 485 log.warning(u"FIXME: unmanaged action {}".format(action))
235 file_data = content_data['application_data']['file_data'] 510 file_data = content_data['application_data']['file_data']
236 try: 511 try:
237 file_elt = elt.elements((NS_JINGLE_FT, 'file')).next() 512 file_elt = elt.elements((NS_JINGLE_FT, 'file')).next()
238 except StopIteration: 513 except StopIteration:
239 raise exceptions.DataError 514 raise exceptions.DataError
240 algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt) 515 algo, file_data['given_file_hash'] = self._hash.parseHashElt(file_elt)
241 if algo != file_data.get('hash_algo'): 516 if algo != file_data.get('hash_algo'):
242 log.warning(u"Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo})" 517 log.warning(u"Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo}) [{profile}]"
243 .format(peer_algo=algo, our_algo=file_data.get('hash_algo'))) 518 .format(peer_algo=algo, our_algo=file_data.get('hash_algo'), profile=client.profile))
244 else: 519 else:
245 self._receiverTryTerminate(client, session, content_name, content_data) 520 self._receiverTryTerminate(client, session, content_name, content_data)
246 else: 521 else:
247 raise NotImplementedError 522 raise NotImplementedError
523
524 def jingleTerminate(self, client, action, session, content_name, jingle_elt):
525 if jingle_elt.decline:
526 # progress is the only way to tell to frontends that session has been declined
527 progress_id = self.getProgressId(session, content_name)
528 self.host.bridge.progressError(progress_id, C.PROGRESS_ERROR_DECLINED, client.profile)
248 529
249 def _sendCheckSum(self, client, session, content_name, content_data): 530 def _sendCheckSum(self, client, session, content_name, content_data):
250 """Send the session-info with the hash checksum""" 531 """Send the session-info with the hash checksum"""
251 file_data = content_data['application_data']['file_data'] 532 file_data = content_data['application_data']['file_data']
252 hasher = file_data['hash_hasher'] 533 hasher = file_data['hash_hasher']
270 @return (bool): True if session was terminated 551 @return (bool): True if session was terminated
271 """ 552 """
272 if not content_data.get('transfer_finished', False): 553 if not content_data.get('transfer_finished', False):
273 return False 554 return False
274 file_data = content_data['application_data']['file_data'] 555 file_data = content_data['application_data']['file_data']
275 hash_given = file_data.get('hash_given') 556 given_hash = file_data.get('given_file_hash')
276 if hash_given is None: 557 if given_hash is None:
277 if last_try: 558 if last_try:
278 log.warning(u"sender didn't sent hash checksum, we can't check the file") 559 log.warning(u"sender didn't sent hash checksum, we can't check the file [{profile}]".format(profile=client.profile))
279 self._j.delayedContentTerminate(client, session, content_name) 560 self._j.delayedContentTerminate(client, session, content_name)
280 content_data['stream_object'].close() 561 content_data['stream_object'].close()
281 return True 562 return True
282 return False 563 return False
283 hasher = file_data['hash_hasher'] 564 hasher = file_data['hash_hasher']
284 hash_ = hasher.hexdigest() 565 hash_ = hasher.hexdigest()
285 566
286 if hash_ == hash_given: 567 if hash_ == given_hash:
287 log.info(u"Hash checked, file was successfully transfered: {}".format(hash_)) 568 log.info(u"Hash checked, file was successfully transfered: {}".format(hash_))
288 progress_metadata = {'hash': hash_, 569 progress_metadata = {'hash': hash_,
289 'hash_algo': file_data['hash_algo'], 570 'hash_algo': file_data['hash_algo'],
290 'hash_verified': C.BOOL_TRUE 571 'hash_verified': C.BOOL_TRUE
291 } 572 }
293 else: 574 else:
294 log.warning(u"Hash mismatch, the file was not transfered correctly") 575 log.warning(u"Hash mismatch, the file was not transfered correctly")
295 progress_metadata=None 576 progress_metadata=None
296 error = u"Hash mismatch: given={algo}:{given}, calculated={algo}:{our}".format( 577 error = u"Hash mismatch: given={algo}:{given}, calculated={algo}:{our}".format(
297 algo = file_data['hash_algo'], 578 algo = file_data['hash_algo'],
298 given = hash_given, 579 given = given_hash,
299 our = hash_) 580 our = hash_)
300 581
301 self._j.delayedContentTerminate(client, session, content_name) 582 self._j.delayedContentTerminate(client, session, content_name)
302 content_data['stream_object'].close(progress_metadata, error) 583 content_data['stream_object'].close(progress_metadata, error)
303 # we may have the last_try timer still active, so we try to cancel it 584 # we may have the last_try timer still active, so we try to cancel it