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)