comparison src/plugins/plugin_xep_0260.py @ 1570:37d4be4a9fed

plugins XEP-0260, XEP-0065: proxy handling: - XEP-0065: Candidate.activate launch proxy activation - XEP-0065: a candidate is individually connected with connectCandidate - transport-info action handling can now manage candidate and proxy infos
author Goffi <goffi@goffi.org>
date Sun, 08 Nov 2015 14:44:33 +0100
parents 268fda4236ca
children c668081eba1c
comparison
equal deleted inserted replaced
1569:44854fb5d3b2 1570:37d4be4a9fed
44 "dependencies": ["XEP-0166", "XEP-0065"], 44 "dependencies": ["XEP-0166", "XEP-0065"],
45 "main": "XEP_0260", 45 "main": "XEP_0260",
46 "handler": "yes", 46 "handler": "yes",
47 "description": _("""Implementation of Jingle SOCKS5 Bytestreams""") 47 "description": _("""Implementation of Jingle SOCKS5 Bytestreams""")
48 } 48 }
49
50
51 class ProxyError(Exception):
52 pass
49 53
50 54
51 class XEP_0260(object): 55 class XEP_0260(object):
52 # TODO: udp handling 56 # TODO: udp handling
53 57
125 mode = 'tcp' # XXX: we only manage tcp for now 129 mode = 'tcp' # XXX: we only manage tcp for now
126 transport_elt = self._buildCandidates(session, candidates, sid, session_hash, client, mode) 130 transport_elt = self._buildCandidates(session, candidates, sid, session_hash, client, mode)
127 131
128 defer.returnValue(transport_elt) 132 defer.returnValue(transport_elt)
129 133
134 def _proxyActivatedCb(self, iq_result_elt, candidate, session, content_name, profile):
135 """Called when activation confirmation has been received from proxy
136
137 cf XEP-0260 § 2.4
138 """
139 # now that the proxy is activated, we have to inform other peer
140 iq_elt, transport_elt = self._j.buildAction(self._j.A_TRANSPORT_INFO, session, content_name, profile)
141 activated_elt = transport_elt.addElement('activated')
142 activated_elt['cid'] = candidate.id
143 iq_elt.send
144
145 def _proxyActivatedEb(self, stanza_error, candidate, session, content_name, profile):
146 """Called when activation error has been received from proxy
147
148 cf XEP-0260 § 2.4
149 """
150 # TODO: fallback to IBB
151 # now that the proxy is activated, we have to inform other peer
152 iq_elt, transport_elt = self._j.buildAction(self._j.A_TRANSPORT_INFO, session, content_name, profile)
153 transport_elt.addElement('proxy-error')
154 iq_elt.send
155 return stanza_error
156
130 def _foundPeerCandidate(self, candidate, session, transport_data, content_name, client): 157 def _foundPeerCandidate(self, candidate, session, transport_data, content_name, client):
131 """Called when the best candidate from other peer is found 158 """Called when the best candidate from other peer is found
132 159
133 @param candidate(XEP_0065.Candidate, None): selected candidate, 160 @param candidate(XEP_0065.Candidate, None): selected candidate,
134 or None if no candidate is accessible 161 or None if no candidate is accessible
135 @param session(dict): session data 162 @param session(dict): session data
136 @param transport_data(dict): transport data 163 @param transport_data(dict): transport data
137 @param content_name(dict): name of the current content 164 @param content_name(unicode): name of the current content
138 @param client(unicode): %(doc_client)s 165 @param client(unicode): %(doc_client)s
139 """ 166 """
140 167
141 transport_data['best_candidate'] = candidate 168 transport_data['best_candidate'] = candidate
142 # we need to disconnect all non selected candidates before removing them 169 # we need to disconnect all non selected candidates before removing them
152 else: 179 else:
153 log.info(u"Found best peer candidate: {}".format(unicode(candidate))) 180 log.info(u"Found best peer candidate: {}".format(unicode(candidate)))
154 candidate_elt = transport_elt.addElement('candidate-used') 181 candidate_elt = transport_elt.addElement('candidate-used')
155 candidate_elt['cid'] = candidate.id 182 candidate_elt['cid'] = candidate.id
156 iq_elt.send() # TODO: check result stanza 183 iq_elt.send() # TODO: check result stanza
157 content_data = session['contents'][content_name] 184 self._checkCandidates(session, content_name, transport_data, client)
158 self._checkCandidates(session, content_data, transport_data, client) 185
159 186 def _checkCandidates(self, session, content_name, transport_data, client):
160 def _checkCandidates(self, session, content_data, transport_data, client):
161 """Called when a candidate has been choosed 187 """Called when a candidate has been choosed
162 188
163 if we have both candidates, we select one, or fallback to an other transport 189 if we have both candidates, we select one, or fallback to an other transport
164 @param session(dict): session data 190 @param session(dict): session data
165 @param content_data(dict): content data 191 @param content_name(unicode): name of the current content
166 @param transport_data(dict): transport data 192 @param transport_data(dict): transport data
167 @param client(unicode): %(doc_client)s 193 @param client(unicode): %(doc_client)s
168 """ 194 """
195 content_data = session['contents'][content_name]
169 try: 196 try:
170 best_candidate = transport_data['best_candidate'] 197 best_candidate = transport_data['best_candidate']
171 except KeyError: 198 except KeyError:
172 # we have not our best candidate yet 199 # we have not our best candidate yet
173 return 200 return
192 choosed_candidate = max(best_candidate, peer_best_candidate, key=lambda c:c.priority) 219 choosed_candidate = max(best_candidate, peer_best_candidate, key=lambda c:c.priority)
193 220
194 if choosed_candidate is None: 221 if choosed_candidate is None:
195 log.warning(u"Socks5 negociation failed, we need to fallback to IBB") 222 log.warning(u"Socks5 negociation failed, we need to fallback to IBB")
196 else: 223 else:
197 if choosed_candidate==best_candidate: 224 # best_peer_candidate was choosed from the candidates we have sent
198 who = u'our' 225 # so our_candidate is true if choosed_candidate is peer_best_candidate
199 else: 226 our_candidate = choosed_candidate==peer_best_candidate
200 who = u'other peer'
201 best_candidate.discard()
202 227
203 log.info(u"Socks5 negociation successful, {who} candidate will be used: {candidate}".format( 228 log.info(u"Socks5 negociation successful, {who} candidate will be used: {candidate}".format(
204 who = who, 229 who = u'our' if our_candidate else u'other peer',
205 candidate = choosed_candidate)) 230 candidate = choosed_candidate))
206 del transport_data['best_candidate'] 231 del transport_data['best_candidate']
207 del transport_data['peer_best_candidate'] 232 del transport_data['peer_best_candidate']
233
234 if choosed_candidate.type == self._s5b.TYPE_PROXY:
235 # the file transfer need to wait for proxy activation
236 # (see XEP-0260 § 2.4)
237 if our_candidate:
238 d = self._s5b.connectCandidate(choosed_candidate, transport_data['session_hash'], profile=client.profile)
239 d.addCallback(lambda dummy: choosed_candidate.activate(transport_data['sid'], session['peer_jid'], client))
240 args = [choosed_candidate, session, content_name, client.profile]
241 d.addCallbacks(self._proxyActivatedCb, self._proxyActivatedEb, args, None, args)
242 else:
243 # this Deferred will be called when we'll receive activation confirmation from other peer
244 d = transport_data['activation_d'] = defer.Deferred()
245 else:
246 d = defer.succeed(None)
247
208 if content_data['senders'] == session['role']: 248 if content_data['senders'] == session['role']:
209 # we can now start the file transfer 249 # we can now start the file transfer (or start it after proxy activation)
210 choosed_candidate.startTransfer(transport_data['session_hash']) 250 d.addCallback(lambda dummy: choosed_candidate.startTransfer(transport_data['session_hash']))
251
252 def _candidateInfo(self, candidate_elt, session, content_name, transport_data, client):
253 """Called when best candidate has been received from peer (or if none is working)
254
255 @param candidate_elt(domish.Element): candidate-used or candidate-error element
256 (see XEP-0260 §2.3)
257 @param session(dict): session data
258 @param content_name(unicode): name of the current content
259 @param transport_data(dict): transport data
260 @param client(unicode): %(doc_client)s
261 """
262 if candidate_elt.name == 'candidate-error':
263 # candidate-error, no candidate worked
264 transport_data['peer_best_candidate'] = None
265 else:
266 # candidate-used, one candidate was choosed
267 try:
268 cid = candidate_elt.attributes['cid']
269 except KeyError:
270 log.warning(u"No cid found in <candidate-used>")
271 raise exceptions.DataError
272 try:
273 candidate = (c for c in transport_data['candidates'] if c.id == cid).next()
274 except StopIteration:
275 log.warning(u"Given cid doesn't correspond to any known candidate !")
276 raise exceptions.DataError # TODO: send an error to other peer, and use better exception
277 except KeyError:
278 # a transport-info can also be intentionaly sent too early by other peer
279 # but there is little probability
280 log.error(u'"candidates" key doesn\'t exists in transport_data, it should at this point')
281 raise exceptions.InternalError
282 # at this point we have the candidate choosed by other peer
283 transport_data['peer_best_candidate'] = candidate
284 log.info(u"Other peer best candidate: {}".format(candidate))
285
286 del transport_data['candidates']
287 self._checkCandidates(session, content_name, transport_data, client)
288
289 def _proxyActivationInfo(self, proxy_elt, session, content_name, transport_data, client):
290 """Called when proxy has been activated (or has sent an error)
291
292 @param proxy_elt(domish.Element): <activated/> or <proxy-error/> element
293 (see XEP-0260 §2.4)
294 @param session(dict): session data
295 @param content_name(unicode): name of the current content
296 @param transport_data(dict): transport data
297 @param client(unicode): %(doc_client)s
298 """
299 try:
300 activation_d = transport_data.pop('activation_d')
301 except KeyError:
302 log.warning(u"Received unexpected transport-info for proxy activation")
303
304 if proxy_elt.name == 'activated':
305 activation_d.callback(None)
306 else:
307 activation_d.errback(ProxyError)
211 308
212 @defer.inlineCallbacks 309 @defer.inlineCallbacks
213 def jingleHandler(self, action, session, content_name, transport_elt, profile): 310 def jingleHandler(self, action, session, content_name, transport_elt, profile):
214 client = self.host.getClient(profile) 311 client = self.host.getClient(profile)
215 content_data = session['contents'][content_name] 312 content_data = session['contents'][content_name]
254 transport_data['candidates'] = candidates 351 transport_data['candidates'] = candidates
255 # we can now build a new <transport> element with our candidates 352 # we can now build a new <transport> element with our candidates
256 transport_elt = self._buildCandidates(session, candidates, sid, session_hash, client) 353 transport_elt = self._buildCandidates(session, candidates, sid, session_hash, client)
257 354
258 elif action == self._j.A_TRANSPORT_INFO: 355 elif action == self._j.A_TRANSPORT_INFO:
259 # other peer gave us its choosed candidate 356 # transport-info can be about candidate or proxy activation
260 try: 357 candidate_elt = None
261 candidate_elt = transport_elt.elements(NS_JINGLE_S5B, 'candidate-used').next() 358
262 except StopIteration: 359 for method, names in ((self._candidateInfo, ('candidate-used', 'candidate-error')),
263 try: 360 (self._proxyActivationInfo, ('activated', 'proxy-error'))):
264 candidate_elt = transport_elt.elements(NS_JINGLE_S5B, 'candidate-error').next() 361 for name in names:
265 except StopIteration: 362 try:
266 log.warning(u"Unexpected transport element: {}".format(transport_elt.toXml())) 363 candidate_elt = transport_elt.elements(NS_JINGLE_S5B, name).next()
267 raise exceptions.DataError 364 except StopIteration:
268 else: 365 continue
269 # candidate-error, no candidate worked 366 else:
270 transport_data['peer_best_candidate'] = None 367 method(candidate_elt, session, content_name, transport_data, client)
271 else: 368 break
272 # candidate-used, one candidate was choosed 369
273 try: 370 if candidate_elt is None:
274 cid = candidate_elt.attributes['cid'] 371 log.warning(u"Unexpected transport element: {}".format(transport_elt.toXml()))
275 except KeyError:
276 log.warning(u"No cid found in <candidate-used>")
277 raise exceptions.DataError
278 try:
279 candidate = (c for c in transport_data['candidates'] if c.id == cid).next()
280 except StopIteration:
281 log.warning(u"Given cid doesn't correspond to any known candidate !")
282 raise exceptions.DataError # TODO: send an error to other peer, and use better exception
283 except KeyError:
284 # a transport-info can also be intentionaly sent too early by other peer
285 # but there is little probability
286 log.error(u'"candidates" key doesn\'t exists in transport_data, it should at this point')
287 raise exceptions.InternalError
288 # at this point we have the candidate choosed by other peer
289 transport_data['peer_best_candidate'] = candidate
290 log.info(u"Other peer best candidate: {}".format(candidate))
291
292 del transport_data['candidates']
293 self._checkCandidates(session, content_data, transport_data, client)
294 372
295 else: 373 else:
296 log.warning(u"FIXME: unmanaged action {}".format(action)) 374 log.warning(u"FIXME: unmanaged action {}".format(action))
297 375
298 defer.returnValue(transport_elt) 376 defer.returnValue(transport_elt)