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