Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0234.py @ 1620:4dd07d026214
plugin XEP-0234: hash checksum proper handling
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 17 Nov 2015 20:13:27 +0100 |
parents | 0de5f210fe56 |
children | 63cef4dbf2a4 |
comparison
equal
deleted
inserted
replaced
1619:3ec7511dbf28 | 1620:4dd07d026214 |
---|---|
29 from twisted.words.xish import domish | 29 from twisted.words.xish import domish |
30 from twisted.words.protocols.jabber import jid | 30 from twisted.words.protocols.jabber import jid |
31 from twisted.python import failure | 31 from twisted.python import failure |
32 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | 32 from twisted.words.protocols.jabber.xmlstream import XMPPHandler |
33 from twisted.internet import defer | 33 from twisted.internet import defer |
34 from twisted.internet import reactor | |
35 from twisted.internet import error as internet_error | |
34 | 36 |
35 | 37 |
36 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4' | 38 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4' |
37 | 39 |
38 PLUGIN_INFO = { | 40 PLUGIN_INFO = { |
47 } | 49 } |
48 | 50 |
49 | 51 |
50 class XEP_0234(object): | 52 class XEP_0234(object): |
51 # TODO: assure everything is closed when file is sent or session terminate is received | 53 # TODO: assure everything is closed when file is sent or session terminate is received |
52 # TODO: call self._f.unregister when unloading order will be managing (i.e. when depenencies will be unloaded at the end) | 54 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end) |
53 | 55 |
54 def __init__(self, host): | 56 def __init__(self, host): |
55 log.info(_("plugin Jingle File Transfer initialization")) | 57 log.info(_("plugin Jingle File Transfer initialization")) |
56 self.host = host | 58 self.host = host |
57 self._j = host.plugins["XEP-0166"] # shortcut to access jingle | 59 self._j = host.plugins["XEP-0166"] # shortcut to access jingle |
137 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) | 139 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) |
138 except StopIteration: | 140 except StopIteration: |
139 file_data[name] = '' | 141 file_data[name] = '' |
140 | 142 |
141 try: | 143 try: |
144 hash_algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt) | |
145 except exceptions.NotFound: | |
146 raise failure.Failure(exceptions.DataError) | |
147 | |
148 if hash_algo is not None: | |
149 file_data['hash_algo'] = hash_algo | |
150 file_data['hash_hasher'] = hasher = self._hash.getHasher(hash_algo) | |
151 file_data['data_cb'] = lambda data: hasher.update(data) | |
152 | |
153 try: | |
142 file_data['size'] = int(file_data['size']) | 154 file_data['size'] = int(file_data['size']) |
143 except ValueError: | 155 except ValueError: |
144 raise failure.Failure(exceptions.DataError) | 156 raise failure.Failure(exceptions.DataError) |
145 | 157 |
146 name = file_data['name'] | 158 name = file_data['name'] |
147 if '/' in name or '\\' in name: | 159 if '/' in name or '\\' in name: |
148 log.warning(u"File name contain path characters, we replace them: {}".format(name)) | 160 log.warning(u"File name contain path characters, we replace them: {}".format(name)) |
149 file_data['name'] = name.replace('/', '_').replace('\\', '_') | 161 file_data['name'] = name.replace('/', '_').replace('\\', '_') |
150 | |
151 # TODO: parse hash using plugin XEP-0300 | |
152 | 162 |
153 content_data['application_data']['file_data'] = file_data | 163 content_data['application_data']['file_data'] = file_data |
154 | 164 |
155 # now we actualy request permission to user | 165 # now we actualy request permission to user |
156 def gotConfirmation(confirmed): | 166 def gotConfirmation(confirmed): |
161 return confirmed | 171 return confirmed |
162 | 172 |
163 d = self._f.getDestDir(session['peer_jid'], content_data, file_data, profile) | 173 d = self._f.getDestDir(session['peer_jid'], content_data, file_data, profile) |
164 d.addCallback(gotConfirmation) | 174 d.addCallback(gotConfirmation) |
165 return d | 175 return d |
166 | |
167 | 176 |
168 def jingleHandler(self, action, session, content_name, desc_elt, profile): | 177 def jingleHandler(self, action, session, content_name, desc_elt, profile): |
169 content_data = session['contents'][content_name] | 178 content_data = session['contents'][content_name] |
170 application_data = content_data['application_data'] | 179 application_data = content_data['application_data'] |
171 if action in (self._j.A_ACCEPTED_ACK,): | 180 if action in (self._j.A_ACCEPTED_ACK,): |
178 # initiator doesn't manage <range>, but we do so we advertise it | 187 # initiator doesn't manage <range>, but we do so we advertise it |
179 log.debug("adding <range> element") | 188 log.debug("adding <range> element") |
180 file_elt.addElement('range') | 189 file_elt.addElement('range') |
181 elif action == self._j.A_SESSION_ACCEPT: | 190 elif action == self._j.A_SESSION_ACCEPT: |
182 assert not 'file_obj' in content_data | 191 assert not 'file_obj' in content_data |
192 file_data = application_data['file_data'] | |
183 file_path = application_data['file_path'] | 193 file_path = application_data['file_path'] |
184 size = application_data['file_data']['size'] | 194 size = file_data['size'] |
195 # XXX: hash security is not critical here, so we just take the higher mandatory one | |
196 hasher = file_data['hash_hasher'] = self._hash.getHasher('sha-256') | |
185 content_data['file_obj'] = self._f.File(self.host, | 197 content_data['file_obj'] = self._f.File(self.host, |
186 file_path, | 198 file_path, |
187 uid=self._getProgressId(session, content_name), | 199 uid=self._getProgressId(session, content_name), |
188 size=size, | 200 size=size, |
201 data_cb=lambda data: hasher.update(data), | |
189 profile=profile | 202 profile=profile |
190 ) | 203 ) |
191 finished_d = content_data['finished_d'] = defer.Deferred() | 204 finished_d = content_data['finished_d'] = defer.Deferred() |
192 args = [session, content_name, content_data, profile] | 205 args = [session, content_name, content_data, profile] |
193 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) | 206 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) |
194 else: | 207 else: |
195 log.warning(u"FIXME: unmanaged action {}".format(action)) | 208 log.warning(u"FIXME: unmanaged action {}".format(action)) |
196 return desc_elt | 209 return desc_elt |
197 | 210 |
211 def jingleSessionInfo(self, action, session, content_name, jingle_elt, profile): | |
212 """Called on session-info action | |
213 | |
214 manage checksum, and ignore <received/> element | |
215 """ | |
216 # TODO: manage <received/> element | |
217 content_data = session['contents'][content_name] | |
218 elts = [elt for elt in jingle_elt.elements() if elt.uri == NS_JINGLE_FT] | |
219 if not elts: | |
220 return | |
221 for elt in elts: | |
222 if elt.name == 'received': | |
223 pass | |
224 elif elt.name == 'checksum': | |
225 # we have received the file hash, we need to parse it | |
226 if content_data['senders'] == session['role']: | |
227 log.warning(u"unexpected checksum received while we are the file sender") | |
228 raise exceptions.DataError | |
229 info_content_name = elt['name'] | |
230 if info_content_name != content_name: | |
231 # it was for an other content... | |
232 return | |
233 file_data = content_data['application_data']['file_data'] | |
234 try: | |
235 file_elt = elt.elements((NS_JINGLE_FT, 'file')).next() | |
236 except StopIteration: | |
237 raise exceptions.DataError | |
238 algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt) | |
239 if algo != file_data.get('hash_algo'): | |
240 log.warning(u"Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo})" | |
241 .format(peer_algo=algo, our_algo=file_data.get('hash_algo'))) | |
242 else: | |
243 self._receiverTryTerminate(session, content_name, content_data, profile=profile) | |
244 else: | |
245 raise NotImplementedError | |
246 | |
247 def _sendCheckSum(self, session, content_name, content_data, profile): | |
248 """Send the session-info with the hash checksum""" | |
249 file_data = content_data['application_data']['file_data'] | |
250 hasher = file_data['hash_hasher'] | |
251 hash_ = hasher.hexdigest() | |
252 log.debug(u"Calculated hash: {}".format(hash_)) | |
253 iq_elt, jingle_elt = self._j.buildSessionInfo(session, profile) | |
254 checksum_elt = jingle_elt.addElement((NS_JINGLE_FT, 'checksum')) | |
255 checksum_elt['creator'] = content_data['creator'] | |
256 checksum_elt['name'] = content_name | |
257 file_elt = checksum_elt.addElement('file') | |
258 file_elt.addChild(self._hash.buildHashElt(hash_)) | |
259 iq_elt.send() | |
260 | |
261 def _receiverTryTerminate(self, session, content_name, content_data, last_try=False, profile=C.PROF_KEY_NONE): | |
262 """Try to terminate the session | |
263 | |
264 This method must only be used by the receiver. | |
265 It check if transfer is finished, and hash available, | |
266 if everything is OK, it check hash and terminate the session | |
267 @param last_try(bool): if True this mean than session must be terminated even given hash is not available | |
268 @return (bool): True if session was terminated | |
269 """ | |
270 if not content_data.get('transfer_finished', False): | |
271 return False | |
272 file_data = content_data['application_data']['file_data'] | |
273 hash_given = file_data.get('hash_given') | |
274 if hash_given is None: | |
275 if last_try: | |
276 log.warning(u"sender didn't sent hash checksum, we can't check the file") | |
277 self._j.delayedContentTerminate(session, content_name, profile=profile) | |
278 content_data['file_obj'].close() | |
279 return True | |
280 return False | |
281 hasher = file_data['hash_hasher'] | |
282 hash_ = hasher.hexdigest() | |
283 | |
284 if hash_ == hash_given: | |
285 log.info(u"Hash checked, file was successfully transfered: {}".format(hash_)) | |
286 else: | |
287 log.warning(u"Hash mismatch, the file was not transfered correctly") | |
288 | |
289 self._j.delayedContentTerminate(session, content_name, profile=profile) | |
290 content_data['file_obj'].close() | |
291 # we may have the last_try timer still active, so we try to cancel it | |
292 try: | |
293 content_data['last_try_timer'].cancel() | |
294 except (KeyError, internet_error.AlreadyCalled): | |
295 pass | |
296 return True | |
297 | |
198 def _finishedCb(self, dummy, session, content_name, content_data, profile): | 298 def _finishedCb(self, dummy, session, content_name, content_data, profile): |
199 log.info(u"File transfer completed successfuly") | 299 log.info(u"File transfer completed") |
200 if content_data['senders'] != session['role']: | 300 if content_data['senders'] != session['role']: |
201 # we terminate the session only if we are the received, | 301 # we terminate the session only if we are the receiver, |
202 # as recommanded in XEP-0234 §2 (after example 6) | 302 # as recommanded in XEP-0234 §2 (after example 6) |
203 self._j.contentTerminate(session, content_name, profile=profile) | 303 content_data['transfer_finished'] = True |
204 content_data['file_obj'].close() | 304 if not self._receiverTryTerminate(session, content_name, content_data, profile=profile): |
305 # we have not received the hash yet, we wait 5 more seconds | |
306 content_data['last_try_timer'] = reactor.callLater( | |
307 5, self._receiverTryTerminate, session, content_name, content_data, last_try=True, profile=profile) | |
308 else: | |
309 # we are the sender, we send the checksum | |
310 self._sendCheckSum(session, content_name, content_data, profile) | |
311 content_data['file_obj'].close() | |
205 | 312 |
206 def _finishedEb(self, failure, session, content_name, content_data, profile): | 313 def _finishedEb(self, failure, session, content_name, content_data, profile): |
207 log.warning(u"Error while streaming through s5b: {}".format(failure)) | 314 log.warning(u"Error while streaming through s5b: {}".format(failure)) |
208 content_data['file_obj'].close() | 315 content_data['file_obj'].close() |
209 self._j.contentTerminate(session, content_name, reason=self._j.REASON_FAILED_TRANSPORT, profile=profile) | 316 self._j.contentTerminate(session, content_name, reason=self._j.REASON_FAILED_TRANSPORT, profile=profile) |