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