1# New API for making it easier to write Jingle tests. The idea
2# is not so much to hide away the details (this makes tests
3# unreadable), but to make the expressions denser and more concise.
4# Helper classes support different dialects so the test can
5# be invoked for different (possibly all) dialects.
6
7from functools import partial
8from twisted.words.xish import domish, xpath
9import random
10from gabbletest import sync_stream, exec_test
11from servicetest import EventPattern
12import dbus
13import ns
14import os
15import constants as cs
16
17class JingleProtocol(object):
18    """
19    Defines a simple DSL for constructing Jingle messages.
20    """
21
22    def __init__(self, dialect):
23        self.dialect = dialect
24        self.id_seq = 0
25
26    def _simple_xml(self, node):
27        "Construct domish.Element tree from tree of tuples"
28        name, namespace, attribs, children = node
29        el = domish.Element((namespace, name))
30        for key, val in attribs.items():
31            el[key] = val
32        for c in children:
33            if isinstance(c, tuple):
34                el.addChild(self._simple_xml(c))
35            elif isinstance(c, unicode):
36                el.addContent(c)
37            else:
38                raise ValueError("invalid child object %r of type %r" % (c, type(c)))
39        return el
40
41    def xml(self, node):
42        "Returns XML from tree of tuples"
43        return self._simple_xml(node).toXml()
44
45    def Iq(self, type, id, frm, to, children):
46        "Creates an IQ element"
47        if not id:
48            id = 'seq%d' % self.id_seq
49            self.id_seq += 1
50
51        return ('iq', 'jabber:client',
52            { 'type': type, 'from': frm, 'to': to, 'id': id },
53            children)
54
55    def SetIq(self, frm, to, children):
56        "Creates a set IQ element"
57        return self.Iq('set', None, frm, to, children)
58
59    def ResultIq(self, to, iq, children):
60        "Creates a result IQ element"
61        return self.Iq('result', iq['id'], iq['to'], to,
62            children)
63
64    def ErrorIq(self, iq, errtype, errchild):
65        "Creates an error IQ element, and includes the original stanza"
66        return self.Iq('error', iq['id'], iq['to'], iq['from'],
67            [ iq.firstChildElement(),
68                ('error', None, { 'type': errtype, 'xmlns': ns.STANZA, },
69                    [ errchild ]) ])
70
71    def PayloadType(self, name, rate, id, parameters={}, **kw):
72        "Creates a <payload-type> element"
73        kw['name'] = name
74        kw['rate'] = rate
75        kw['id'] = id
76        children = [self.Parameter(name, value)
77                    for name, value in parameters.iteritems()]
78        return ('payload-type', None, kw, children)
79
80    def Parameter(self, name, value):
81        "Creates a <parameter> element"
82        return ('parameter', None, {'name': name, 'value': value}, [])
83
84    def TransportGoogleP2PCall (self, username, password, call_remote_candidates=[]):
85        candidates = []
86        for (component, host, port, props) in call_remote_candidates:
87
88            candidates.append(("candidate", None, {
89                "name": "rtp",
90                "address": host,
91                "port": str(port),
92                "protocol": "udp",
93                "preference": str(props["priority"] / 65536.0),
94                "type":  ["INVALID NONE", "local", "stun", "INVALID PEER RFLX", "relay"][props["type"]],
95                "network": "0",
96                "generation": "0",# Increment this yourself if you care.
97                "component": str(component), # 1 is rtp, 2 is rtcp
98                "username": props.get("username", username),
99                "password": props.get("password", password),
100                }, [])) #NOTE: subtype and profile are unused
101        return ('transport', ns.GOOGLE_P2P, {}, candidates)
102
103    def TransportGoogleP2P(self, remote_transports=[]):
104        """
105        Creates a <transport> element for Google P2P transport.
106        If remote_transports is present, and of the form
107        [(host, port, proto, subtype, profile, pref, transtype, user, pwd)]
108        (basically a list of Media_Stream_Handler_Transport without the
109        component number) then it will be converted to xml and added.
110        """
111        candidates = []
112        for i, (host, port, proto, subtype, profile, pref, transtype, user, pwd
113                ) in enumerate(remote_transports):
114            candidates.append(("candidate", None, {
115                "name": "rtp",
116                "address": host,
117                "port": str(port),
118                "protocol": ["udp", "tcp"][proto],
119                "preference": str(pref),
120                "type":  ["local", "stun", "relay"][transtype],
121                "network": "0",
122                "generation": "0",# Increment this yourself if you care.
123                "component": "1", # 1 is rtp, 2 is rtcp
124                "username": user,
125                "password": pwd,
126                }, [])) #NOTE: subtype and profile are unused
127        return ('transport', ns.GOOGLE_P2P, {}, candidates)
128
129    def TransportIceUdp(self, remote_transports=[]):
130        """
131        Creates a <transport> element for ICE-UDP transport.
132        If remote_transports is present, and of the form
133        [(host, port, proto, subtype, profile, pref, transtype, user, pwd)]
134        (basically a list of Media_Stream_Handler_Transport without the
135        component number) then it will be converted to xml and added.
136        """
137        candidates = []
138        attrs = {}
139        for (host, port, proto, subtype, profile, pref, transtype, user, pwd
140                ) in remote_transports:
141            if "ufrag" not in attrs:
142                attrs = {"ufrag": user, "pwd": pwd}
143            else:
144                assert (user == attrs["ufrag"] and pwd == attrs["pwd"]
145                    ), "user and pwd should be the same across all candidates."
146
147            node = ("candidate", None, {
148                # ICE-CORE says it can be arbitrary string, even though XEP
149                # gives an int as an example.
150                "foundation": "fake",
151                "ip": host,
152                "port": str(port),
153                "protocol": ["udp", "tcp"][proto],
154                # Gabble multiplies by 65536 so we should too.
155                "priority": str(int(pref * 65536)),
156                "type":  ["host", "srflx", "srflx"][transtype],
157                "network": "0",
158                "generation": "0",# Increment this yourself if you care.
159                "component": "1", # 1 is rtp, 2 is rtcp
160                }, []) #NOTE: subtype and profile are unused
161            candidates.append(node)
162        return ('transport', ns.JINGLE_TRANSPORT_ICEUDP, attrs, candidates)
163
164    def Presence(self, frm, to, caps):
165        "Creates <presence> stanza with specified capabilities"
166        children = []
167        if caps:
168            children = [ ('c', ns.CAPS, caps, []) ]
169        return ('presence', 'jabber:client', { 'from': frm, 'to': to },
170            children)
171
172    def Query(self, node, xmlns, children):
173        "Creates <query> element"
174        attrs = {}
175        if node:
176            attrs['node'] = node
177        return ('query', xmlns, attrs, children)
178
179    def Feature(self, var):
180        "Creates <feature> element"
181        return ('feature', None, { 'var': var }, [])
182
183    def action_predicate(self, action):
184        def f(e):
185            return self.match_jingle_action(e.query, action)
186
187        return f
188
189    def match_jingle_action(self, q, action):
190        return q is not None and q.name == 'jingle' and q['action'] == action
191
192    def _extract_session_id(self, query):
193        return query['sid']
194
195    def validate_session_initiate(self, query):
196        raise NotImplementedError()
197
198    def can_do_video(self):
199        return True
200
201    def can_do_video_only(self):
202        return self.can_do_video()
203
204    def separate_contents(self):
205        return True
206
207    def has_mutable_streams(self):
208        return True
209
210    def is_modern_jingle(self):
211        return False
212
213    def rtp_info_event(self, name):
214        return None
215
216    def rtp_info_event_list(self, name):
217        e = self.rtp_info_event(name)
218        return [e] if e is not None else []
219
220
221class GtalkProtocol03(JingleProtocol):
222    features = [ ns.GOOGLE_FEAT_VOICE, ns.GOOGLE_FEAT_VIDEO ]
223
224    def __init__(self):
225        JingleProtocol.__init__(self, 'gtalk-v0.3')
226
227    def _action_map(self, action):
228        map = {
229            'session-initiate': 'initiate',
230            'session-terminate': 'terminate',
231            'session-accept': 'accept',
232            'transport-info': 'candidates'
233        }
234
235        if action in map:
236            return map[action]
237        else:
238            return action
239
240    def Jingle(self, sid, initiator, action, children):
241        action = self._action_map(action)
242        return ('session', ns.GOOGLE_SESSION,
243            { 'type': action, 'initiator': initiator, 'id': sid }, children)
244
245    def PayloadType(self, name, rate, id, parameters={}, **kw):
246        p = JingleProtocol.PayloadType(self, name, rate, id, parameters,
247            **kw)
248        if "type" in kw:
249            namespaces = { "audio": ns.GOOGLE_SESSION_PHONE,
250                           "video": ns.GOOGLE_SESSION_VIDEO,
251            }
252            p = p[:1] + (namespaces[kw["type"]],) + p[2:]
253
254        return p
255
256    # Gtalk has only one content, and <content> node is implicit. Also it
257    # never mixes payloads and transport information. It's up to the call of
258    # this function to ensure it never calls it with both mixed
259    def Content(self, name, creator, senders=None,
260            description=None, transport=None):
261        # Normally <content> has <description> and <transport>, but we only
262        # use <description> unless <transport> has candidates.
263        assert description == None or len(transport[3]) == 0
264
265        if description != None:
266            return description
267        else:
268            assert len(transport[3]) == 1, \
269                    "gtalk 0.3 only lets you send one candidate at a time." \
270                    "You sent %r" % [transport]
271            return transport[3][0]
272
273    def Description(self, type, children):
274        if type == 'audio':
275            namespace = ns.GOOGLE_SESSION_PHONE
276        elif type == 'video':
277            namespace = ns.GOOGLE_SESSION_VIDEO
278        else:
279            namespace = 'unexistent-namespace'
280        return ('description', namespace, {}, children)
281
282    def match_jingle_action(self, q, action):
283        action = self._action_map(action)
284        return q is not None and q.name == 'session' and q['type'] == action
285
286    def _extract_session_id(self, query):
287        return query['id']
288
289    def can_do_video_only(self):
290        return False
291
292    def validate_session_initiate(self, query):
293        sid = self._extract_session_id(query)
294
295        # No transport in GTalk03
296        assert xpath.queryForNodes('/session/transport', query) == None
297
298        # Exactly one description in Gtalk03
299        descs = xpath.queryForNodes('/session/description', query)
300        assert len(descs) == 1
301
302        desc = descs[0]
303
304        # the ds is either audio or video
305        assert desc.uri in [ ns.GOOGLE_SESSION_PHONE, ns.GOOGLE_SESSION_VIDEO ]
306
307        if desc.uri == ns.GOOGLE_SESSION_VIDEO:
308            # If it's a video call there should be some audio codecs as well
309            assert xpath.queryForNodes(
310                '/session/description/payload-type[@xmlns="%s"]' %
311                    ns.GOOGLE_SESSION_PHONE, query)
312            return (sid, ['fake-audio'], ['fake-video'])
313        else:
314            return (sid, ['fake-audio'], [])
315
316    def separate_contents(self):
317        return False
318
319    def has_mutable_streams(self):
320        return False
321
322class GtalkProtocol04(JingleProtocol):
323    features = [ ns.GOOGLE_FEAT_VOICE, ns.GOOGLE_P2P ]
324
325    def __init__(self):
326        JingleProtocol.__init__(self, 'gtalk-v0.4')
327
328    def _action_map(self, action):
329        map = {
330            'session-initiate': 'initiate',
331            'session-terminate': 'terminate',
332            'session-accept': 'accept',
333        }
334
335        if action in map:
336            return map[action]
337        else:
338            return action
339
340    def Jingle(self, sid, initiator, action, children):
341        # ignore Content and go straight for its children
342        if len(children) == 1 and children[0][0] == 'dummy-content':
343            # Either have just a transport or a description + transport
344            # without candidates
345            children = children[0][3]
346
347        action = self._action_map(action)
348        return ('session', ns.GOOGLE_SESSION,
349            { 'type': action, 'initiator': initiator, 'id': sid }, children)
350
351    # hacky: parent Jingle node should just pick up our children
352    def Content(self, name, creator, senders=None,
353            description=None, transport=None):
354        return ('dummy-content', None, {},
355            [node for node in [description, transport] if node != None])
356
357    def Description(self, type, children):
358        return ('description', ns.GOOGLE_SESSION_PHONE, {}, children)
359
360    def match_jingle_action(self, q, action):
361        action = self._action_map(action)
362        return q is not None and q.name == 'session' and q['type'] == action
363
364    def _extract_session_id(self, query):
365        return query['id']
366
367    def validate_session_initiate(self, query):
368        # FIXME: validate it!
369        return (self._extract_session_id(query), ['fake-audio'], [])
370
371    def can_do_video(self):
372        return False
373
374class JingleProtocol015(JingleProtocol):
375    features = [ ns.GOOGLE_P2P, ns.JINGLE_015, ns.JINGLE_015_AUDIO,
376        ns.JINGLE_015_VIDEO ]
377
378    def __init__(self):
379        JingleProtocol.__init__(self, 'jingle-v0.15')
380
381    def Jingle(self, sid, initiator, action, children):
382        return ('jingle', ns.JINGLE_015,
383            { 'action': action, 'initiator': initiator, 'sid': sid }, children)
384
385    # Note: senders weren't mandatory in this dialect
386    def Content(self, name, creator, senders = None,
387            description=None, transport=None):
388        attribs = { 'name': name, 'creator': creator }
389        if senders:
390            attribs['senders'] = senders
391        return ('content', None, attribs,
392            [node for node in [description, transport] if node != None])
393
394    def Description(self, type, children):
395        if type == 'audio':
396            namespace = ns.JINGLE_015_AUDIO
397        elif type == 'video':
398            namespace = ns.JINGLE_015_VIDEO
399        else:
400            namespace = 'unexistent-namespace'
401        return ('description', namespace, { 'type': type }, children)
402
403    def validate_session_initiate(self, query):
404        contents = xpath.queryForNodes(
405            '/jingle[@xmlns="%s"]/content' % ns.JINGLE_015,
406            query)
407
408        audio, video = [], []
409
410        for c in contents:
411            a_desc = xpath.queryForNodes(
412                '/content/description[@xmlns="%s"]' % ns.JINGLE_015_AUDIO,
413                c)
414            v_desc = xpath.queryForNodes(
415                '/content/description[@xmlns="%s"]' % ns.JINGLE_015_VIDEO,
416                c)
417
418            if a_desc is not None:
419                assert len(a_desc) == 1, c.toXml()
420                assert v_desc is None
421                audio.append(c['name'])
422            elif v_desc is not None:
423                assert len(v_desc) == 1, c.toXml()
424                assert a_desc is None
425                video.append(c['name'])
426            else:
427                assert False, c.toXml()
428
429        assert len(audio) + len(video) > 0, query.toXml()
430
431        return (self._extract_session_id(query), audio, video)
432
433class JingleProtocol031(JingleProtocol):
434    features = [ ns.JINGLE, ns.JINGLE_RTP, ns.JINGLE_RTP_AUDIO,
435        ns.JINGLE_RTP_VIDEO, ns.GOOGLE_P2P ]
436
437    def __init__(self):
438        JingleProtocol.__init__(self, 'jingle-v0.31')
439
440    def Jingle(self, sid, initiator, action, children):
441        return ('jingle', ns.JINGLE,
442            { 'action': action, 'initiator': initiator, 'sid': sid }, children)
443
444    def Content(self, name, creator, senders=None,
445            description=None, transport=None):
446        if not senders:
447            senders = 'both'
448        return ('content', None,
449            { 'name': name, 'creator': creator, 'senders': senders },
450            [node for node in [description, transport] if node != None])
451
452    def Description(self, type, children):
453        return ('description', ns.JINGLE_RTP, { 'media': type }, children)
454
455    def is_modern_jingle(self):
456        return True
457
458    def rtp_info_event(self, name):
459        def p(e):
460            query = e.query
461            if not self.match_jingle_action(query, 'session-info'):
462                return False
463            n = query.firstChildElement()
464            return n is not None and n.uri == ns.JINGLE_RTP_INFO_1 and \
465                n.name == name
466
467        return EventPattern('stream-iq', predicate=p)
468
469    def validate_session_initiate(self, query):
470        contents = xpath.queryForNodes(
471            '/jingle[@xmlns="%s"]/content' % ns.JINGLE,
472            query)
473
474        audio, video = [], []
475
476        for c in contents:
477            descs = xpath.queryForNodes(
478                '/content/description[@xmlns="%s"]' % ns.JINGLE_RTP,
479                c)
480
481            assert len(descs) == 1, c.toXml()
482
483            d = descs[0]
484
485            if d['media'] == 'audio':
486                audio.append(c['name'])
487            elif d['media'] == 'video':
488                video.append(c['name'])
489            else:
490                assert False, c.toXml()
491
492        assert len(audio) + len(video) > 0, query.toXml()
493
494        return (self._extract_session_id(query), audio, video)
495
496class JingleTest2(object):
497    # Default caps for the remote end
498    remote_caps = { 'ext': '', 'ver': '0.0.0',
499             'node': 'http://example.com/fake-client0' }
500
501    # Default audio codecs for the remote end
502    audio_codecs = [ ('GSM', 3, 8000, {}),
503        ('PCMA', 8, 8000, {}),
504        ('PCMU', 0, 8000, {}) ]
505
506    # Default video codecs for the remote end. I have no idea what's
507    # a suitable value here...
508    video_codecs = [ ('WTF', 96, 90000, {}) ]
509
510
511    ufrag = "SessionUfrag"
512    pwd = "SessionPwd"
513    # Default candidates for the remote end
514    remote_call_candidates = [# Local candidates
515                         (1, "192.168.0.1", 666,
516                            {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST,
517                             #"Foundation":,
518                             "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP,
519                             "priority": 10000,
520                             #"base-ip":
521                             }),
522                         (2, "192.168.0.1", 667,
523                            {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST,
524                             #"Foundation":,
525                             "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP,
526                             "priority": 10000,
527                             #"base-ip":
528                             }),
529                         # STUN candidates have their own ufrag
530                         (1, "168.192.0.1", 10666,
531                            {"type": cs.CALL_STREAM_CANDIDATE_TYPE_SERVER_REFLEXIVE,
532                             #"Foundation":,
533                             "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP,
534                             "priority": 100,
535                             #"base-ip":,
536                             "username": "STUNRTPUfrag",
537                             "password": "STUNRTPPwd"
538                             }),
539                         (2, "168.192.0.1", 10667,
540                            {"type": cs.CALL_STREAM_CANDIDATE_TYPE_SERVER_REFLEXIVE,
541                             #"Foundation":,
542                             "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP,
543                             "priority": 100,
544                             #"base-ip":,
545                             "username": "STUNRTCPUfrag",
546                             "password": "STUNRTCPPwd"
547                             }),
548                         # Candidates found using UPnP or somesuch?
549                         (1, "131.111.12.50", 10666,
550                            {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST,
551                             #"Foundation":,
552                             "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP,
553                             "priority": 1000,
554                             #"base-ip":
555                             }),
556                         (2, "131.111.12.50", 10667,
557                            {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST,
558                             #"Foundation":,
559                             "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP,
560                             "priority": 1000,
561                             #"base-ip":
562                             }),
563                             ]
564    remote_transports = [
565          ( "192.168.0.1", # host
566            666, # port
567            0, # protocol = TP_MEDIA_STREAM_BASE_PROTO_UDP
568            "RTP", # protocol subtype
569            "AVP", # profile
570            1.0, # preference
571            0, # transport type = TP_CALL_STREAM_CANDIDATE_TYPE_HOST,
572            "username",
573            "password" ) ]
574
575
576
577    def __init__(self, jp, conn, q, stream, jid, peer):
578        self.jp = jp
579        self.conn = conn
580        self.q = q
581        self.jid = jid
582        self.peer = peer
583        self.peer_bare_jid = peer.split('/', 1)[0]
584        self.stream = stream
585        self.sid = 'sess' + str(int(random.random() * 10000))
586
587    def prepare(self, send_presence=True, send_roster=True, events=None):
588        # If we need to override remote caps, feats, codecs or caps,
589        # we should do it prior to calling this method.
590
591        if events is None:
592            # Catch events: authentication, our presence update,
593            # status connected, vCard query
594            # If we don't catch the vCard query here, it can trip us up later:
595            # http://bugs.freedesktop.org/show_bug.cgi?id=19161
596            events = self.q.expect_many(
597                    EventPattern('stream-iq', to=None, query_ns='vcard-temp',
598                        query_name='vCard'),
599                    EventPattern('stream-iq', query_ns=ns.ROSTER),
600                    )
601
602        # some Jingle tests care about our roster relationship to the peer
603        if send_roster:
604            roster = events[-1]
605
606            roster.stanza['type'] = 'result'
607            item = roster.query.addElement('item')
608            item['jid'] = 'publish@foo.com'
609            item['subscription'] = 'from'
610            item = roster.query.addElement('item')
611            item['jid'] = 'subscribe@foo.com'
612            item['subscription'] = 'to'
613            item = roster.query.addElement('item')
614            item['jid'] = 'publish-subscribe@foo.com'
615            item['subscription'] = 'both'
616            self.stream.send(roster.stanza)
617
618        if send_presence:
619            self.send_presence_and_caps()
620
621    def send_presence(self):
622        # We need remote end's presence for capabilities
623        self.stream.send(self.jp.xml(
624            self.jp.Presence(self.peer, self.jid, self.remote_caps)))
625
626        # Gabble doesn't trust it, so makes a disco
627        return self.q.expect('stream-iq', query_ns=ns.DISCO_INFO, to=self.peer)
628
629    def send_remote_disco_reply(self, query_stanza):
630        self.stream.send(self.jp.xml(self.jp.ResultIq(self.jid, query_stanza,
631            [ self.jp.Query(None, ns.DISCO_INFO,
632                [ self.jp.Feature(x) for x in self.jp.features ]) ]) ))
633
634    def send_presence_and_caps(self):
635        event = self.send_presence()
636        self.send_remote_disco_reply(event.stanza)
637
638        # Force Gabble to process the caps before doing any more Jingling
639        sync_stream(self.q, self.stream)
640
641    def generate_payloads(self, codecs, **kwargs):
642        return [ self.jp.PayloadType(payload_name,
643            str(rate), str(id), parameters, **kwargs) for
644                    (payload_name, id, rate, parameters) in codecs ]
645
646    def generate_contents(self, transports=[]):
647        assert len(self.audio_names + self.video_names) > 0
648
649        jp = self.jp
650
651        assert len(self.video_names) == 0 or jp.can_do_video()
652
653        contents = []
654
655        if not jp.separate_contents() and self.video_names:
656            assert jp.can_do_video()
657            assert self.audio_names
658
659            payload = self.generate_payloads (self.video_codecs) + \
660                self.generate_payloads (self.audio_codecs, type="audio")
661
662            contents.append(
663                jp.Content('stream0', 'initiator', 'both',
664                    jp.Description('video', payload),
665                    jp.TransportGoogleP2P(transports))
666             )
667        else:
668            def mk_content(name, media, codecs):
669                payload = self.generate_payloads (codecs)
670
671                contents.append(
672                    jp.Content(name, 'initiator', 'both',
673                        jp.Description(media, payload),
674                    jp.TransportGoogleP2P(transports))
675                )
676
677            for name in self.audio_names:
678                mk_content(name, 'audio', self.audio_codecs)
679
680            for name in self.video_names:
681                mk_content(name, 'video', self.video_codecs)
682
683        return contents
684
685    def incoming_call(self, audio = "audio1", video = None):
686        jp = self.jp
687
688        self.audio_names = [ audio ] if audio != None else []
689        self.video_names = [ video ] if video != None else []
690
691        contents = self.generate_contents()
692
693        node = jp.SetIq(self.peer, self.jid, [
694            jp.Jingle(self.sid, self.peer, 'session-initiate', contents),
695            ])
696        self.stream.send(jp.xml(node))
697
698    def parse_session_initiate (self, query):
699        # Validate the session initiate and get some useful info from it
700        self.sid, self.audio_names, self.video_names = \
701            self.jp.validate_session_initiate(query)
702
703    def accept(self):
704        jp = self.jp
705
706        contents = self.generate_contents()
707        node = jp.SetIq(self.peer, self.jid, [
708            jp.Jingle(self.sid, self.peer, 'session-accept',
709                contents) ])
710        self.stream.send(jp.xml(node))
711
712    def content_accept(self, query, media):
713        """
714        Accepts a content-add stanza containing a single <content> of the given
715        media type.
716        """
717        jp = self.jp
718        assert jp.separate_contents()
719        c = query.firstChildElement()
720
721        if media == 'audio':
722            codecs = self.audio_codecs
723        elif media == 'video':
724            codecs = self.video_codecs
725        else:
726            assert False
727
728        # Remote end finally accepts
729        node = jp.SetIq(self.peer, self.jid, [
730            jp.Jingle(self.sid, self.peer, 'content-accept', [
731                jp.Content(c['name'], c['creator'], c['senders'],
732                    jp.Description(media, [
733                        jp.PayloadType(name, str(rate), str(id), parameters) for
734                            (name, id, rate, parameters) in codecs ]),
735                jp.TransportGoogleP2P()) ]) ])
736        self.stream.send(jp.xml(node))
737
738    def content_modify(self, name, creator, senders):
739        jp = self.jp
740
741        assert jp.separate_contents()
742        node = jp.SetIq(self.peer, self.jid, [
743            jp.Jingle(self.sid, self.peer, 'content-modify', [
744                jp.Content(name, creator, senders)])])
745        self.stream.send(jp.xml(node))
746
747
748    def terminate(self, reason=None, text=""):
749        jp = self.jp
750
751        if reason is not None and jp.is_modern_jingle():
752            body = [("reason", None, {},
753                        [(reason, None, {}, []),
754                         ("text", None, {}, [text]),
755                        ]
756                    )]
757        else:
758            body = []
759
760        iq = jp.SetIq(self.peer, self.jid, [
761            jp.Jingle(self.sid, self.peer, 'session-terminate', body) ])
762        self.stream.send(jp.xml(iq))
763
764    def result_iq(self, iniq, children = []):
765        jp = self.jp
766        iq = jp.ResultIq(self.peer, {'id': iniq.iq_id, 'to': self.peer},
767                         children)
768        self.stream.send(jp.xml(iq))
769
770
771    def send_remote_candidates_call_xmpp(self, name, creator, candidates=None):
772        jp = self.jp
773        if candidates is None:
774            candidates = self.remote_call_candidates
775
776        node = jp.SetIq(self.peer, self.jid,
777            [ jp.Jingle(self.sid, self.peer, 'transport-info',
778                [ jp.Content(name, creator,
779                    transport=jp.TransportGoogleP2PCall (self.ufrag, self.pwd,
780                                                 candidates))
781                ] )
782            ])
783        self.stream.send(jp.xml(node))
784
785    def remote_candidates(self, name, creator):
786        jp = self.jp
787
788        node = jp.SetIq(self.peer, self.jid,
789            [ jp.Jingle(self.sid, self.peer, 'transport-info',
790                [ jp.Content(name, creator,
791                    transport=jp.TransportGoogleP2P (self.remote_transports))
792                ] )
793            ])
794        self.stream.send(jp.xml(node))
795
796    def dbusify_codecs(self, codecs):
797        dbussed_codecs = [ (id, name, 0, rate, 0, params )
798                            for (name, id, rate, params) in codecs ]
799        return dbus.Array(dbussed_codecs, signature='(usuuua{ss})')
800
801    def dbusify_codecs_with_params (self, codecs):
802        return self.dbusify_codecs(codecs)
803
804    def get_audio_codecs_dbus(self):
805        return self.dbusify_codecs(self.audio_codecs)
806
807    def get_video_codecs_dbus(self):
808        return self.dbusify_codecs(self.video_codecs)
809
810    def dbusify_call_codecs(self, codecs):
811        dbussed_codecs = [ (id, name, rate, 0, False, params)
812                            for (name, id, rate, params) in codecs ]
813        return dbus.Array(dbussed_codecs, signature='(usuuba{ss})')
814
815    def dbusify_call_codecs_with_params(self, codecs):
816        return dbusify_call_codecs (self, codecs)
817
818    def __get_call_audio_codecs_dbus(self):
819        return self.dbusify_call_codecs(self.audio_codecs)
820
821    def __get_call_video_codecs_dbus(self):
822        return self.dbusify_call_codecs(self.video_codecs)
823
824    def get_call_audio_md_dbus(self, handle = 0):
825        d =  dbus.Dictionary(
826            { cs.CALL_CONTENT_MEDIADESCRIPTION + '.Codecs': self.__get_call_audio_codecs_dbus(),
827            }, signature='sv')
828        if handle != 0:
829            d[cs.CALL_CONTENT_MEDIADESCRIPTION + '.RemoteContact'] = dbus.UInt32 (handle)
830        return d
831
832    def get_call_video_md_dbus(self, handle = 0):
833        d = dbus.Dictionary(
834            { cs.CALL_CONTENT_MEDIADESCRIPTION + '.Codecs': self.__get_call_video_codecs_dbus(),
835            }, signature='sv')
836        if handle != 0:
837            d[cs.CALL_CONTENT_MEDIADESCRIPTION + '.RemoteContact'] = dbus.UInt32 (handle)
838        return d
839
840    def get_remote_transports_dbus(self):
841        return dbus.Array([
842            (dbus.UInt32(1 + i), host, port, proto, subtype,
843                profile, pref, transtype, user, pwd)
844                for i, (host, port, proto, subtype, profile,
845                    pref, transtype, user, pwd)
846                in enumerate(self.remote_transports) ],
847            signature='(usuussduss)')
848
849    def get_call_remote_transports_dbus(self):
850        return dbus.Array(self.remote_call_candidates,
851            signature='(usqa{sv})')
852
853
854def test_dialects(f, dialects, params=None, protocol=None):
855    for dialect in dialects:
856        exec_test(partial(f, dialect()), params=params, protocol=protocol)
857
858def test_all_dialects(f, params=None, protocol=None):
859    dialectmap = { "jingle015": JingleProtocol015,
860        "jingle031": JingleProtocol031,
861        "gtalk03": GtalkProtocol03,
862        "gtalk04":  GtalkProtocol04
863    }
864    dialects = []
865
866    jd = os.getenv("JINGLE_DIALECTS")
867    if jd == None:
868        dialects = dialectmap.values()
869    else:
870        for d in jd.split (','):
871            dialects.append(dialectmap[d])
872    test_dialects(f,  dialects, params=params, protocol=protocol)
873