Package common :: Module jingle_session
[hide private]
[frames] | no frames]

Source Code for Module common.jingle_session

  1  ## 
  2  ## Copyright (C) 2006 Gajim Team 
  3  ## 
  4  ## This program is free software; you can redistribute it and/or modify 
  5  ## it under the terms of the GNU General Public License as published 
  6  ## by the Free Software Foundation; version 2 only. 
  7  ## 
  8  ## This program is distributed in the hope that it will be useful, 
  9  ## but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  ## GNU General Public License for more details. 
 12  ## 
 13   
 14  """ 
 15  Handles Jingle sessions (XEP 0166) 
 16  """ 
 17   
 18  #TODO: 
 19  # * 'senders' attribute of 'content' element 
 20  # * security preconditions 
 21  # * actions: 
 22  #   - content-modify 
 23  #   - session-info 
 24  #   - security-info 
 25  #   - transport-accept, transport-reject 
 26  #   - Tie-breaking 
 27  # * timeout 
 28   
 29  import gajim #Get rid of that? 
 30  import xmpp 
 31  from jingle_transport import get_jingle_transport 
 32  from jingle_content import get_jingle_content, JingleContentSetupException 
 33   
 34  # FIXME: Move it to JingleSession.States? 
35 -class JingleStates(object):
36 """ 37 States in which jingle session may exist 38 """ 39 ended = 0 40 pending = 1 41 active = 2
42
43 -class OutOfOrder(Exception):
44 """ 45 Exception that should be raised when an action is received when in the wrong 46 state 47 """
48
49 -class TieBreak(Exception):
50 """ 51 Exception that should be raised in case of a tie, when we overrule the other 52 action 53 """
54
55 -class JingleSession(object):
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 = {} # negotiated contents 68 self.connection = con # connection to use 69 # our full jid 70 #FIXME: Get rid of gajim here? 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 # jid we connect to 75 # jid we use as the initiator 76 self.initiator = weinitiate and self.ourjid or self.peerjid 77 # jid we use as the responder 78 self.responder = weinitiate and self.peerjid or self.ourjid 79 # are we an initiator? 80 self.weinitiate = weinitiate 81 # what state is session in? (one from JingleStates) 82 self.state = JingleStates.ended 83 if not sid: 84 sid = con.connection.getAnID() 85 self.sid = sid # sessionid 86 87 self.accepted = True # is this session accepted by user 88 89 # callbacks to call on proper contents 90 # use .prepend() to add new callbacks, especially when you're going 91 # to send error instead of ack 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], #TODO 97 'content-modify': [self.__ack], #TODO 98 'content-reject': [self.__ack, self.__on_content_remove], #TODO 99 'content-remove': [self.__ack, self.__on_content_remove], 100 'description-info': [self.__broadcast, self.__ack], #TODO 101 'security-info': [self.__ack], #TODO 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], #TODO 111 'transport-accept': [self.__ack], #TODO 112 'transport-reject': [self.__ack], #TODO 113 'iq-result': [], 114 'iq-error': [self.__on_error], 115 }
116
117 - def approve_session(self):
118 """ 119 Called when user accepts session in UI (when we aren't the initiator) 120 """ 121 self.accept_session()
122
123 - def decline_session(self):
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
145 - def end_session(self):
146 """ 147 Called when user stops or cancel session in UI 148 """ 149 reason = xmpp.Node('reason') 150 if self.state == JingleStates.active: 151 reason.addChild('success') 152 else: 153 reason.addChild('cancel') 154 self._session_terminate(reason)
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 # The content is from us, accept it 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
207 - def on_session_state_changed(self, content=None):
208 if self.state == JingleStates.ended: 209 # Session not yet started, only one action possible: session-initiate 210 if self.is_ready() and self.weinitiate: 211 self.__session_initiate() 212 elif self.state == JingleStates.pending: 213 # We can either send a session-accept or a content-add 214 if self.is_ready() and not self.weinitiate: 215 self.__session_accept() 216 elif content and (content.creator == 'initiator') == self.weinitiate: 217 self.__content_add(content) 218 elif content and self.weinitiate: 219 self.__content_accept(content) 220 elif self.state == JingleStates.active: 221 # We can either send a content-add or a content-accept 222 if not content: 223 return 224 if (content.creator == 'initiator') == self.weinitiate: 225 # We initiated this content. It's a pending content-add. 226 self.__content_add(content) 227 else: 228 # The other side created this content, we accept it. 229 self.__content_accept(content)
230
231 - def is_ready(self):
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
238 - def accept_session(self):
239 """ 240 Mark the session as accepted 241 """ 242 self.accepted = True 243 self.on_session_state_changed()
244
245 - def start_session(self):
246 """ 247 Mark the session as ready to be started 248 """ 249 self.accepted = True 250 self.on_session_state_changed()
251
252 - def send_session_info(self):
253 pass
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
261 - def send_transport_info(self, content):
262 assert self.state != JingleStates.ended 263 stanza, jingle = self.__make_jingle('transport-info') 264 jingle.addChild(node=content) 265 self.connection.connection.send(stanza)
266
267 - def send_description_info(self, content):
268 assert self.state != JingleStates.ended 269 stanza, jingle = self.__make_jingle('description-info') 270 jingle.addChild(node=content) 271 self.connection.connection.send(stanza)
272
273 - def on_stanza(self, stanza):
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 # it's an iq-error stanza 283 action = 'iq-error' 284 elif jingle: 285 # it's a jingle action 286 action = jingle.getAttr('action') 287 if action not in self.callbacks: 288 self.__send_error(stanza, 'bad-request') 289 return 290 # FIXME: If we aren't initiated and it's not a session-initiate... 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 # it's an iq-result (ack) stanza 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 # FIXME 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):
319 # FIXME 320 text = error.getTagData('text') 321 error_name = None 322 for child in error.getChildren(): 323 if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: 324 error_name = child.getName() 325 break 326 elif child.getNamespace() == xmpp.NS_STANZAS: 327 error_name = child.getName() 328 self.__dispatch_error(error_name, text, error.getAttribute('type'))
329 # FIXME: Not sure when we would want to do that... 330
331 - def __on_transport_replace(self, stanza, jingle, error, action):
332 for content in jingle.iterTags('content'): 333 creator = content['creator'] 334 name = content['name'] 335 if (creator, name) in self.contents: 336 transport_ns = content.getTag('transport').getNamespace() 337 if transport_ns == xmpp.JINGLE_ICE_UDP: 338 # FIXME: We don't manage anything else than ICE-UDP now... 339 # What was the previous transport?!? 340 # Anyway, content's transport is not modifiable yet 341 pass 342 else: 343 stanza, jingle = self.__make_jingle('transport-reject') 344 content = jingle.setTag('content', attrs={'creator': creator, 345 'name': name}) 346 content.setTag('transport', namespace=transport_ns) 347 self.connection.connection.send(stanza) 348 raise xmpp.NodeProcessed 349 else: 350 # FIXME: This ressource is unknown to us, what should we do? 351 # For now, reject the transport 352 stanza, jingle = self.__make_jingle('transport-reject') 353 c = jingle.setTag('content', attrs={'creator': creator, 354 'name': name}) 355 c.setTag('transport', namespace=transport_ns) 356 self.connection.connection.send(stanza) 357 raise xmpp.NodeProcessed
358
359 - def __on_session_info(self, stanza, jingle, error, action):
360 # TODO: ringing, active, (un)hold, (un)mute 361 payload = jingle.getPayload() 362 if payload: 363 self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info', type_='modify') 364 raise xmpp.NodeProcessed
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 # TODO: this will fail if content is not an RTP content 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
381 - def __on_session_accept(self, stanza, jingle, error, action):
382 # FIXME 383 if self.state != JingleStates.pending: 384 raise OutOfOrder 385 self.state = JingleStates.active
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 # check which contents are accepted 393 for content in jingle.iterTags('content'): 394 creator = content['creator'] 395 # TODO 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 # TODO 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
416 - def __on_session_initiate(self, stanza, jingle, error, action):
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 # user did not accept this session yet 428 429 # TODO: If the initiator is unknown to the receiver (e.g., via presence 430 # subscription) and the receiver has a policy of not communicating via 431 # Jingle with unknown entities, it SHOULD return a <service-unavailable/> 432 # error. 433 434 # Check if there's already a session with this user: 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 # Lets check what kind of jingle session does the peer want 445 contents, contents_rejected, reason_txt = self.__parse_contents(jingle) 446 447 # If there's no content we understand... 448 if not contents: 449 # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate 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 # Send event about starting a session 459 self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, 460 contents))
461
462 - def __broadcast(self, stanza, jingle, error, action):
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
477 - def __on_session_terminate(self, stanza, jingle, error, action):
478 self.connection.delete_jingle_session(self.sid) 479 reason, text = self.__reason_from_stanza(jingle) 480 if reason not in ('success', 'cancel', 'decline'): 481 self.__dispatch_error(reason, text) 482 if text: 483 text = '%s (%s)' % (reason, text) 484 else: 485 # TODO 486 text = reason 487 self.connection.dispatch('JINGLE_DISCONNECTED', 488 (self.peerjid, self.sid, None, text))
489
490 - def __broadcast_all(self, stanza, jingle, error, action):
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 # TODO: Needs some reworking 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 # Store the first reason of failure 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
533 - def __dispatch_error(self, error=None, text=None, type_=None):
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
540 - def __reason_from_stanza(self, stanza):
541 # TODO: Move to GUI? 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
557 - def __make_jingle(self, action, reason=None):
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):
571 err_stanza = xmpp.Error(stanza, '%s %s' % (xmpp.NS_STANZAS, error)) 572 err = err_stanza.getTag('error') 573 if type_: 574 err.setAttr('type', type_) 575 if jingle_error: 576 err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) 577 if text: 578 err.setTagData('text', text) 579 self.connection.connection.send(err_stanza) 580 self.__dispatch_error(jingle_error or error, text, type_)
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 # TODO: integrate with __appendContent? 595 # TODO: parameters 'name', 'content'? 596 for content in self.contents.values(): 597 self.__append_content(jingle, content)
598
599 - def __session_initiate(self):
600 assert self.state == JingleStates.ended 601 stanza, jingle = self.__make_jingle('session-initiate') 602 self.__append_contents(jingle) 603 self.__broadcast(stanza, jingle, None, 'session-initiate-sent') 604 self.connection.connection.send(stanza) 605 self.state = JingleStates.pending
606
607 - def __session_accept(self):
608 assert self.state == JingleStates.pending 609 stanza, jingle = self.__make_jingle('session-accept') 610 self.__append_contents(jingle) 611 self.__broadcast(stanza, jingle, None, 'session-accept-sent') 612 self.connection.connection.send(stanza) 613 self.state = JingleStates.active
614
615 - def __session_info(self, payload=None):
616 assert self.state != JingleStates.ended 617 stanza, jingle = self.__make_jingle('session-info') 618 if payload: 619 jingle.addChild(node=payload) 620 self.connection.connection.send(stanza)
621
622 - def _session_terminate(self, reason=None):
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 # TODO: Move to GUI? 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 # TODO: test 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 # TODO: test 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 # TODO: this will fail if content is not an RTP content 662 self.connection.dispatch('JINGLE_DISCONNECTED', 663 (self.peerjid, self.sid, content.media, 'rejected'))
664
665 - def __content_modify(self):
666 assert self.state != JingleStates.ended
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 # TODO: this will fail if content is not an RTP content 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