1
2
3
4
5
6
7
8
9
10
11
12
13
14 """
15 Handles Jingle sessions (XEP 0166)
16 """
17
18
19
20
21
22
23
24
25
26
27
28
29 import gajim
30 import xmpp
31 from jingle_transport import get_jingle_transport
32 from jingle_content import get_jingle_content, JingleContentSetupException
33
34
36 """
37 States in which jingle session may exist
38 """
39 ended = 0
40 pending = 1
41 active = 2
42
44 """
45 Exception that should be raised when an action is received when in the wrong
46 state
47 """
48
50 """
51 Exception that should be raised in case of a tie, when we overrule the other
52 action
53 """
54
56 """
57 This represents one jingle session, that is, one or more content types
58 negotiated between an initiator and a responder.
59 """
60
61 - def __init__(self, con, weinitiate, jid, sid=None):
62 """
63 con -- connection object,
64 weinitiate -- boolean, are we the initiator?
65 jid - jid of the other entity
66 """
67 self.contents = {}
68 self.connection = con
69
70
71 self.ourjid = gajim.get_jid_from_account(self.connection.name)
72 if con.server_resource:
73 self.ourjid = self.ourjid + '/' + con.server_resource
74 self.peerjid = jid
75
76 self.initiator = weinitiate and self.ourjid or self.peerjid
77
78 self.responder = weinitiate and self.peerjid or self.ourjid
79
80 self.weinitiate = weinitiate
81
82 self.state = JingleStates.ended
83 if not sid:
84 sid = con.connection.getAnID()
85 self.sid = sid
86
87 self.accepted = True
88
89
90
91
92 self.callbacks = {
93 'content-accept': [self.__on_content_accept, self.__broadcast,
94 self.__ack],
95 'content-add': [self.__on_content_add, self.__broadcast,
96 self.__ack],
97 'content-modify': [self.__ack],
98 'content-reject': [self.__ack, self.__on_content_remove],
99 'content-remove': [self.__ack, self.__on_content_remove],
100 'description-info': [self.__broadcast, self.__ack],
101 'security-info': [self.__ack],
102 'session-accept': [self.__on_session_accept, self.__on_content_accept,
103 self.__broadcast, self.__ack],
104 'session-info': [self.__broadcast, self.__on_session_info, self.__ack],
105 'session-initiate': [self.__on_session_initiate, self.__broadcast,
106 self.__ack],
107 'session-terminate': [self.__on_session_terminate, self.__broadcast_all,
108 self.__ack],
109 'transport-info': [self.__broadcast, self.__ack],
110 'transport-replace': [self.__broadcast, self.__on_transport_replace],
111 'transport-accept': [self.__ack],
112 'transport-reject': [self.__ack],
113 'iq-result': [],
114 'iq-error': [self.__on_error],
115 }
116
118 """
119 Called when user accepts session in UI (when we aren't the initiator)
120 """
121 self.accept_session()
122
124 """
125 Called when user declines session in UI (when we aren't the initiator)
126 """
127 reason = xmpp.Node('reason')
128 reason.addChild('decline')
129 self._session_terminate(reason)
130
131 - def approve_content(self, media):
132 content = self.get_content(media)
133 if content:
134 content.accepted = True
135 self.on_session_state_changed(content)
136
137 - def reject_content(self, media):
138 content = self.get_content(media)
139 if content:
140 if self.state == JingleStates.active:
141 self.__content_reject(content)
142 content.destroy()
143 self.on_session_state_changed()
144
155
156 - def get_content(self, media=None):
157 if media is None:
158 return
159
160 for content in self.contents.values():
161 if content.media == media:
162 return content
163
164 - def add_content(self, name, content, creator='we'):
165 """
166 Add new content to session. If the session is active, this will send
167 proper stanza to update session
168
169 Creator must be one of ('we', 'peer', 'initiator', 'responder')
170 """
171 assert creator in ('we', 'peer', 'initiator', 'responder')
172
173 if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \
174 not self.weinitiate):
175 creator = 'initiator'
176 elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \
177 not self.weinitiate):
178 creator = 'responder'
179 content.creator = creator
180 content.name = name
181 self.contents[(creator, name)] = content
182
183 if (creator == 'initiator') == self.weinitiate:
184
185 content.accepted = True
186
187 - def remove_content(self, creator, name, reason=None):
188 """
189 Remove the content `name` created by `creator`
190 by sending content-remove, or by sending session-terminate if
191 there is no content left.
192 """
193 if (creator, name) in self.contents:
194 content = self.contents[(creator, name)]
195 if len(self.contents) > 1:
196 self.__content_remove(content, reason)
197 self.contents[(creator, name)].destroy()
198 if not self.contents:
199 self.end_session()
200
201 - def modify_content(self, creator, name, *someother):
202 """
203 We do not need this now
204 """
205 pass
206
230
232 """
233 Return True when all codecs and candidates are ready (for all contents)
234 """
235 return (all((content.is_ready() for content in self.contents.itervalues()))
236 and self.accepted)
237
244
246 """
247 Mark the session as ready to be started
248 """
249 self.accepted = True
250 self.on_session_state_changed()
251
254
255 - def send_content_accept(self, content):
256 assert self.state != JingleStates.ended
257 stanza, jingle = self.__make_jingle('content-accept')
258 jingle.addChild(node=content)
259 self.connection.connection.send(stanza)
260
266
272
274 """
275 A callback for ConnectionJingle. It gets stanza, then tries to send it to
276 all internally registered callbacks. First one to raise
277 xmpp.NodeProcessed breaks function
278 """
279 jingle = stanza.getTag('jingle')
280 error = stanza.getTag('error')
281 if error:
282
283 action = 'iq-error'
284 elif jingle:
285
286 action = jingle.getAttr('action')
287 if action not in self.callbacks:
288 self.__send_error(stanza, 'bad-request')
289 return
290
291 if action != 'session-initiate' and self.state == JingleStates.ended:
292 self.__send_error(stanza, 'item-not-found', 'unknown-session')
293 return
294 else:
295
296 action = 'iq-result'
297
298 callables = self.callbacks[action]
299
300 try:
301 for callable in callables:
302 callable(stanza=stanza, jingle=jingle, error=error, action=action)
303 except xmpp.NodeProcessed:
304 pass
305 except TieBreak:
306 self.__send_error(stanza, 'conflict', 'tiebreak')
307 except OutOfOrder:
308
309 self.__send_error(stanza, 'unexpected-request', 'out-of-order')
310
311 - def __ack(self, stanza, jingle, error, action):
312 """
313 Default callback for action stanzas -- simple ack and stop processing
314 """
315 response = stanza.buildReply('result')
316 self.connection.connection.send(response)
317
318 - def __on_error(self, stanza, jingle, error, action):
329
330
358
365
366 - def __on_content_remove(self, stanza, jingle, error, action):
367 for content in jingle.iterTags('content'):
368 creator = content['creator']
369 name = content['name']
370 if (creator, name) in self.contents:
371 content = self.contents[(creator, name)]
372
373 self.connection.dispatch('JINGLE_DISCONNECTED',
374 (self.peerjid, self.sid, content.media, 'removed'))
375 content.destroy()
376 if not self.contents:
377 reason = xmpp.Node('reason')
378 reason.setTag('success')
379 self._session_terminate(reason)
380
386
387 - def __on_content_accept(self, stanza, jingle, error, action):
388 """
389 Called when we get content-accept stanza or equivalent one (like
390 session-accept)
391 """
392
393 for content in jingle.iterTags('content'):
394 creator = content['creator']
395
396 name = content['name']
397
398 - def __on_content_add(self, stanza, jingle, error, action):
399 if self.state == JingleStates.ended:
400 raise OutOfOrder
401
402 parse_result = self.__parse_contents(jingle)
403 contents = parse_result[0]
404 rejected_contents = parse_result[1]
405
406 for name, creator in rejected_contents:
407
408 content = JingleContent()
409 self.add_content(name, content, creator)
410 self.__content_reject(content)
411 self.contents[(content.creator, content.name)].destroy()
412
413 self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid,
414 contents))
415
417 """
418 We got a jingle session request from other entity, therefore we are the
419 receiver... Unpack the data, inform the user
420 """
421 if self.state != JingleStates.ended:
422 raise OutOfOrder
423
424 self.initiator = jingle['initiator']
425 self.responder = self.ourjid
426 self.peerjid = self.initiator
427 self.accepted = False
428
429
430
431
432
433
434
435 for session in self.connection.iter_jingle_sessions(self.peerjid):
436 if not session is self:
437 reason = xmpp.Node('reason')
438 alternative_session = reason.setTag('alternative-session')
439 alternative_session.setTagData('sid', session.sid)
440 self.__ack(stanza, jingle, error, action)
441 self._session_terminate(reason)
442 raise xmpp.NodeProcessed
443
444
445 contents, contents_rejected, reason_txt = self.__parse_contents(jingle)
446
447
448 if not contents:
449
450 reason = xmpp.Node('reason')
451 reason.setTag(reason_txt)
452 self.__ack(stanza, jingle, error, action)
453 self._session_terminate(reason)
454 raise xmpp.NodeProcessed
455
456 self.state = JingleStates.pending
457
458
459 self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid,
460 contents))
461
463 """
464 Broadcast the stanza contents to proper content handlers
465 """
466 for content in jingle.iterTags('content'):
467 name = content['name']
468 creator = content['creator']
469 if (creator, name) not in self.contents:
470 text = 'Content %s (created by %s) does not exist' % (name, creator)
471 self.__send_error(stanza, 'bad-request', text=text, type_='_modify')
472 raise xmpp.NodeProcessed
473 else:
474 cn = self.contents[(creator, name)]
475 cn.on_stanza(stanza, content, error, action)
476
489
491 """
492 Broadcast the stanza to all content handlers
493 """
494 for content in self.contents.itervalues():
495 content.on_stanza(stanza, None, error, action)
496
497 - def __parse_contents(self, jingle):
498
499 contents = []
500 contents_rejected = []
501 reasons = set()
502
503 for element in jingle.iterTags('content'):
504 transport = get_jingle_transport(element.getTag('transport'))
505 content_type = get_jingle_content(element.getTag('description'))
506 if content_type:
507 try:
508 if transport:
509 content = content_type(self, transport)
510 self.add_content(element['name'],
511 content, 'peer')
512 contents.append((content.media,))
513 else:
514 reasons.add('unsupported-transports')
515 contents_rejected.append((element['name'], 'peer'))
516 except JingleContentSetupException:
517 reasons.add('failed-application')
518 else:
519 contents_rejected.append((element['name'], 'peer'))
520 reasons.add('unsupported-applications')
521
522 failure_reason = None
523
524
525 for reason in ('failed-application', 'unsupported-transports',
526 'unsupported-applications'):
527 if reason in reasons:
528 failure_reason = reason
529 break
530
531 return (contents, contents_rejected, failure_reason)
532
534 if text:
535 text = '%s (%s)' % (error, text)
536 if type_ != 'modify':
537 self.connection.dispatch('JINGLE_ERROR',
538 (self.peerjid, self.sid, text or error))
539
541
542 reason = 'success'
543 reasons = ['success', 'busy', 'cancel', 'connectivity-error',
544 'decline', 'expired', 'failed-application', 'failed-transport',
545 'general-error', 'gone', 'incompatible-parameters', 'media-error',
546 'security-error', 'timeout', 'unsupported-applications',
547 'unsupported-transports']
548 tag = stanza.getTag('reason')
549 if tag:
550 text = tag.getTagData('text')
551 for r in reasons:
552 if tag.getTag(r):
553 reason = r
554 break
555 return (reason, text)
556
558 stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid))
559 attrs = {'action': action,
560 'sid': self.sid}
561 if action == 'session-initiate':
562 attrs['initiator'] = self.initiator
563 elif action == 'session-accept':
564 attrs['responder'] = self.responder
565 jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE)
566 if reason is not None:
567 jingle.addChild(node=reason)
568 return stanza, jingle
569
570 - def __send_error(self, stanza, error, jingle_error=None, text=None, type_=None):
581
582 - def __append_content(self, jingle, content):
583 """
584 Append <content/> element to <jingle/> element, with (full=True) or
585 without (full=False) <content/> children
586 """
587 jingle.addChild('content',
588 attrs={'name': content.name, 'creator': content.creator})
589
590 - def __append_contents(self, jingle):
591 """
592 Append all <content/> elements to <jingle/>
593 """
594
595
596 for content in self.contents.values():
597 self.__append_content(jingle, content)
598
606
614
621
623 assert self.state != JingleStates.ended
624 stanza, jingle = self.__make_jingle('session-terminate', reason=reason)
625 self.__broadcast_all(stanza, jingle, None, 'session-terminate-sent')
626 if self.connection.connection and self.connection.connected >= 2:
627 self.connection.connection.send(stanza)
628
629 reason, text = self.__reason_from_stanza(jingle)
630 if reason not in ('success', 'cancel', 'decline'):
631 self.__dispatch_error(reason, text)
632 if text:
633 text = '%s (%s)' % (reason, text)
634 else:
635 text = reason
636 self.connection.delete_jingle_session(self.sid)
637 self.connection.dispatch('JINGLE_DISCONNECTED',
638 (self.peerjid, self.sid, None, text))
639
640 - def __content_add(self, content):
641
642 assert self.state != JingleStates.ended
643 stanza, jingle = self.__make_jingle('content-add')
644 self.__append_content(jingle, content)
645 self.__broadcast(stanza, jingle, None, 'content-add-sent')
646 self.connection.connection.send(stanza)
647
648 - def __content_accept(self, content):
649
650 assert self.state != JingleStates.ended
651 stanza, jingle = self.__make_jingle('content-accept')
652 self.__append_content(jingle, content)
653 self.__broadcast(stanza, jingle, None, 'content-accept-sent')
654 self.connection.connection.send(stanza)
655
656 - def __content_reject(self, content):
657 assert self.state != JingleStates.ended
658 stanza, jingle = self.__make_jingle('content-reject')
659 self.__append_content(jingle, content)
660 self.connection.connection.send(stanza)
661
662 self.connection.dispatch('JINGLE_DISCONNECTED',
663 (self.peerjid, self.sid, content.media, 'rejected'))
664
667
668 - def __content_remove(self, content, reason=None):
669 assert self.state != JingleStates.ended
670 stanza, jingle = self.__make_jingle('content-remove', reason=reason)
671 self.__append_content(jingle, content)
672 self.connection.connection.send(stanza)
673
674 self.connection.dispatch('JINGLE_DISCONNECTED',
675 (self.peerjid, self.sid, content.media, 'removed'))
676
677 - def content_negotiated(self, media):
678 self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid,
679 media))
680