1# vim: set fileencoding=utf-8 :
2import hashlib
3import base64
4import dbus
5
6from avahitest import txt_get_key
7from twisted.words.xish import domish, xpath
8from saluttest import make_result_iq, make_presence, elem_iq, elem
9from servicetest import (
10    EventPattern,
11    assertEquals, assertContains, assertDoesNotContain, assertLength,
12    )
13
14import config
15import ns
16import constants as cs
17
18# The caps we always have, regardless of any clients' caps
19FIXED_CAPS = [
20    ns.JINGLE,
21    ns.JINGLE_015,
22    ns.GOOGLE_FEAT_SESSION,
23    ns.JINGLE_TRANSPORT_RAWUDP,
24    ns.NICK,
25    ns.NICK + '+notify',
26    ns.CHAT_STATES,
27    ns.SI,
28    ns.IBB,
29    ns.BYTESTREAMS,
30    ]
31
32JINGLE_CAPS = [
33    # Additional Jingle transports
34    ns.JINGLE_TRANSPORT_ICEUDP,
35    ns.GOOGLE_P2P,
36    # Jingle content types
37    ns.GOOGLE_FEAT_VOICE,
38    ns.GOOGLE_FEAT_VIDEO,
39    ns.JINGLE_015_AUDIO,
40    ns.JINGLE_015_VIDEO,
41    ns.JINGLE_RTP,
42    ns.JINGLE_RTP_AUDIO,
43    ns.JINGLE_RTP_VIDEO,
44    ]
45
46VARIABLE_CAPS = (
47    JINGLE_CAPS +
48    [
49    ns.FILE_TRANSFER,
50
51    # FIXME: currently we always advertise these, but in future we should
52    # only advertise them if >= 1 client supports them:
53    # ns.TUBES,
54
55    # there is an unlimited set of these; only the ones actually relevant to
56    # the tests so far are shown here
57    ns.TUBES + '/stream#x-abiword',
58    ns.TUBES + '/stream#daap',
59    ns.TUBES + '/stream#http',
60    ns.TUBES + '/dbus#com.example.Go',
61    ns.TUBES + '/dbus#com.example.Xiangqi',
62    ])
63
64def check_caps(namespaces, desired):
65    """Assert that all the FIXED_CAPS are supported, and of the VARIABLE_CAPS,
66    every capability in desired is supported, and every other capability is
67    not.
68    """
69    for c in FIXED_CAPS:
70        assertContains(c, namespaces)
71
72    for c in VARIABLE_CAPS:
73        if c in desired:
74            assertContains(c, namespaces)
75        else:
76            assertDoesNotContain(c, namespaces)
77
78# taken from salut's old caps_helper.py
79def check_caps_txt(txt, ver):
80    for (key, val) in { "1st": "test",
81                        "last": "suite",
82                        "status": "avail",
83                        "txtvers": "1" }.iteritems():
84        v =  txt_get_key(txt, key)
85        assert v == val, (key, val, v)
86
87    assert txt_get_key(txt, "hash") == "sha-1"
88    assert txt_get_key(txt, "node") == ns.GABBLE_CAPS
89
90    v = txt_get_key(txt, "ver")
91    assert v == ver, (v, ver)
92
93text_fixed_properties = dbus.Dictionary({
94    cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
95    cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT
96    })
97text_allowed_properties = dbus.Array([cs.TARGET_HANDLE])
98
99stream_tube_fixed_properties = dbus.Dictionary({
100    cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
101    cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAM_TUBE
102    })
103stream_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE,
104    cs.TARGET_ID, cs.STREAM_TUBE_SERVICE])
105
106dbus_tube_fixed_properties = dbus.Dictionary({
107    cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
108    cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_DBUS_TUBE
109    })
110dbus_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE,
111    cs.TARGET_ID, cs.DBUS_TUBE_SERVICE_NAME])
112
113ft_fixed_properties = dbus.Dictionary({
114    cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
115    cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER,
116    })
117ft_allowed_properties = dbus.Array([
118    cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType',
119    cs.TARGET_HANDLE,
120    cs.TARGET_ID,
121    cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentType',
122    cs.CHANNEL_TYPE_FILE_TRANSFER + '.Filename',
123    cs.CHANNEL_TYPE_FILE_TRANSFER + '.Size',
124    cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash',
125    cs.CHANNEL_TYPE_FILE_TRANSFER + '.Description',
126    cs.CHANNEL_TYPE_FILE_TRANSFER + '.Date',
127    cs.CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset',
128    cs.FT_URI])
129ft_allowed_properties_with_metadata = ft_allowed_properties + [
130    cs.FT_SERVICE_NAME,
131    cs.FT_METADATA]
132
133fake_client_dataforms = {
134    'urn:xmpp:dataforms:softwareinfo':
135    {'software': ['A Fake Client with Twisted'],
136        'software_version': ['5.11.2-svn-20080512'],
137        'os': ['Debian GNU/Linux unstable (sid) unstable sid'],
138        'os_version': ['2.6.24-1-amd64'],
139    },
140}
141
142def compute_caps_hash(identities, features, dataforms):
143    """
144    Accepts a list of slash-separated identities, a list of feature namespaces,
145    and a map from FORM_TYPE to (map from field name to values), returns the
146    verification string as defined by
147    <http://xmpp.org/extensions/xep-0115.html#ver>.
148    """
149    components = []
150
151    for identity in sorted(identities):
152        if len(identity.split('/')) != 4:
153            raise ValueError(
154                "expecting identities of the form " +
155                "'category/type/lang/client': got " + repr(identity))
156
157        components.append(identity)
158
159    for feature in sorted(features):
160        components.append(feature)
161
162    for form_type in sorted(dataforms.keys()):
163        components.append(form_type)
164
165        for var in sorted(dataforms[form_type].keys()):
166            components.append(var)
167
168            for value in sorted(dataforms[form_type][var]):
169                components.append(value)
170
171    components.append('')
172
173    m = hashlib.sha1()
174    S = u'<'.join(components)
175    m.update(S.encode('utf-8'))
176    return base64.b64encode(m.digest())
177
178def make_caps_disco_reply(stream, req, identities, features, dataforms={}):
179    iq = make_result_iq(req)
180    query = iq.firstChildElement()
181
182    for identity in identities:
183        category, type_, lang, name = identity.split('/')
184        el = query.addElement('identity')
185        el['category'] = category
186        el['type'] = type_
187        el['name'] = name
188
189    for f in features:
190        el = domish.Element((None, 'feature'))
191        el['var'] = f
192        query.addChild(el)
193
194    add_dataforms(query, dataforms)
195
196    return iq
197
198def add_dataforms(query, dataforms):
199    for type, fields in dataforms.iteritems():
200        x = query.addElement((ns.X_DATA, 'x'))
201        x['type'] = 'result'
202
203        field = x.addElement('field')
204        field['var'] = 'FORM_TYPE'
205        field['type'] = 'hidden'
206        field.addElement('value', content=type)
207
208        for var, values in fields.iteritems():
209            field = x.addElement('field')
210            field['var'] = var
211
212            for value in values:
213                field.addElement('value', content=value)
214
215def receive_presence_and_ask_caps(q, stream, expect_dbus=True):
216    # receive presence stanza
217    if expect_dbus:
218        presence, event_dbus = q.expect_many(
219                EventPattern('stream-presence'),
220                EventPattern('dbus-signal', signal='ContactCapabilitiesChanged')
221            )
222        assertLength(1, event_dbus.args)
223        signaled_caps = event_dbus.args[0]
224    else:
225        presence = q.expect('stream-presence')
226        signaled_caps = None
227
228    return disco_caps(q, stream, presence) + (signaled_caps,)
229
230def extract_data_forms(x_nodes):
231    dataforms = {}
232
233    if not x_nodes:
234        return dataforms
235
236    for form in x_nodes:
237        name = None
238        fields = {}
239        for field in xpath.queryForNodes('/x/field', form):
240            if field['var'] == 'FORM_TYPE':
241                name = str(field.firstChildElement())
242            else:
243                values = [str(x) for x in xpath.queryForNodes('/field/value', field)]
244
245                fields[field['var']] = values
246
247        if name is not None:
248            dataforms[name] = fields
249
250    return dataforms
251
252def disco_caps(q, stream, txt):
253    hash = txt_get_key(txt, 'hash')
254    ver = txt_get_key(txt, 'ver')
255    node = txt_get_key(txt, 'node')
256    assertEquals('sha-1', hash)
257
258    # ask caps
259    request = \
260        elem_iq(stream, 'get', from_='fake_contact@nearby')(
261          elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver))
262        )
263    stream.send(request)
264
265    # receive caps
266    event = q.expect('stream-iq', query_ns=ns.DISCO_INFO, iq_id=request['id'])
267
268    # Check that Gabble's announcing the identity we think it should be.
269    identity_nodes = xpath.queryForNodes('/iq/query/identity', event.stanza)
270    assertLength(1, identity_nodes)
271    identity_node = identity_nodes[0]
272
273    assertEquals('client', identity_node['category'])
274    assertEquals('pc', identity_node['type'])
275    assertEquals(config.PACKAGE_STRING, identity_node['name'])
276    assertDoesNotContain('xml:lang', identity_node.attributes)
277
278    identity = 'client/%s//%s' % ('pc', config.PACKAGE_STRING)
279
280    features = []
281    for feature in xpath.queryForNodes('/iq/query/feature', event.stanza):
282        features.append(feature['var'])
283
284    # Check if the hash matches the announced capabilities
285    assertEquals(compute_caps_hash([identity], features, {}), ver)
286
287    return (event, features)
288
289def caps_contain(event, cap):
290    node = xpath.queryForNodes('/iq/query/feature[@var="%s"]'
291            % cap,
292            event.stanza)
293    if node is None:
294        return False
295    if len(node) != 1:
296        return False
297    var = node[0].attributes['var']
298    if var is None:
299        return False
300    return var == cap
301
302def presence_and_disco(q, conn, stream, contact, disco,
303                       client, caps,
304                       features, identities=[], dataforms={},
305                       initial=True, show=None):
306    h = send_presence(q, conn, stream, contact, caps, initial=initial,
307        show=show)
308
309    if disco:
310        stanza = expect_disco(q, contact, client, caps)
311        send_disco_reply(stream, stanza, identities, features, dataforms)
312
313    return h
314
315def send_presence(q, conn, stream, contact, caps, initial=True, show=None):
316    h = conn.RequestHandles(cs.HT_CONTACT, [contact])[0]
317
318    if initial:
319        stream.send(make_presence(contact, status='hello'))
320
321        q.expect_many(
322            EventPattern('dbus-signal', signal='PresenceUpdate',
323                args=[{h:
324                   (0L, {u'available': {'message': 'hello'}})}]),
325            EventPattern('dbus-signal', signal='PresencesChanged',
326                args=[{h:
327                   (2, u'available', 'hello')}]))
328
329        # no special capabilities
330        assertEquals([(h, cs.CHANNEL_TYPE_TEXT, 3, 0)],
331            conn.Capabilities.GetCapabilities([h]))
332
333    # send updated presence with caps info
334    stream.send(make_presence(contact, show=show, status='hello', caps=caps))
335
336    return h
337
338def expect_disco(q, contact, client, caps):
339    # Gabble looks up our capabilities
340    event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO)
341    assertEquals(client + '#' + caps['ver'], event.query['node'])
342
343    return event.stanza
344
345def send_disco_reply(stream, stanza, identities, features, dataforms={}):
346    stream.send(
347        make_caps_disco_reply(stream, stanza, identities, features, dataforms))
348
349if __name__ == '__main__':
350    # example from XEP-0115
351    assertEquals('QgayPKawpkPSDYmwT/WM94uAlu0=',
352        compute_caps_hash(['client/pc//Exodus 0.9.1'],
353            ["http://jabber.org/protocol/disco#info",
354             "http://jabber.org/protocol/disco#items",
355             "http://jabber.org/protocol/muc",
356             "http://jabber.org/protocol/caps"],
357            {}))
358
359    # another example from XEP-0115
360    identities = [u'client/pc/en/Psi 0.11', u'client/pc/el/Ψ 0.11']
361    features = [
362        u'http://jabber.org/protocol/caps',
363        u'http://jabber.org/protocol/disco#info',
364        u'http://jabber.org/protocol/disco#items',
365        u'http://jabber.org/protocol/muc',
366        ]
367    dataforms = {
368        u'urn:xmpp:dataforms:softwareinfo':
369            { u'ip_version': [u'ipv4', u'ipv6'],
370              u'os': [u'Mac'],
371              u'os_version': [u'10.5.1'],
372              u'software': [u'Psi'],
373              u'software_version': [u'0.11'],
374            },
375        }
376    assertEquals('q07IKJEyjvHSyhy//CH0CxmKi8w=',
377        compute_caps_hash(identities, features, dataforms))
378