instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / Test / test_buddycast_msg.py
1 # Written by Arno Bakker
2 # see LICENSE.txt for license information
3
4 import unittest
5 import os
6 import sys
7 import time
8 import socket
9 from BaseLib.Core.Utilities.Crypto import sha
10 from traceback import print_exc
11 from types import StringType, ListType, DictType, IntType
12 import tempfile
13
14 from BaseLib.Test.test_as_server import TestAsServer
15 from olconn import OLConnection
16 import btconn
17 from BaseLib.Core.BitTornado.bencode import bencode,bdecode
18 from BaseLib.Core.BitTornado.BT1.MessageID import *
19 from BaseLib.Core.simpledefs import *
20
21 DEBUG=True
22
23
24 class TestBuddyCastMsg(TestAsServer):
25     """ 
26     Testing BUDDYCAST message of BuddyCast extension V1+2+3
27     
28     Note this is based on a reverse-engineering of the protocol.
29     Source code of the specific Tribler release is authoritative.
30     """
31     
32     def setUp(self):
33         """ override TestAsServer """
34         TestAsServer.setUp(self)
35
36     def setUpPreSession(self):
37         """ override TestAsServer """
38         TestAsServer.setUpPreSession(self)
39         # BuddyCast
40         self.config.set_buddycast(True)
41         self.config.set_start_recommender(True)
42         
43         fd,self.superpeerfilename = tempfile.mkstemp()
44         os.write(fd,'')
45         os.close(fd)
46         self.config.set_superpeer_file(self.superpeerfilename)
47
48     def setUpPostSession(self):
49         """ override TestAsServer """
50         TestAsServer.setUpPostSession(self)
51
52         self.mypermid = str(self.my_keypair.pub().get_der())
53         self.hispermid = str(self.his_keypair.pub().get_der())        
54         self.myhash = sha(self.mypermid).digest()
55
56         # Give Tribler some download history
57         print >>sys.stderr,"test: Populating MYPREFERENCES table"
58         self.myprefdb = self.session.open_dbhandler(NTFY_MYPREFERENCES)
59         data = {'destination_path':'.'}
60         infohashes = self.create_good_my_prefs(self,btconn.current_version)
61         for i in range(0,len(infohashes)):
62             commit = (i == len(infohashes)-1) 
63             self.myprefdb.addMyPreference(infohashes[i], data, commit=commit)
64
65         # Give Tribler some peers
66         print >>sys.stderr,"test: Populating PEERS table"
67         self.peerdb = self.session.open_dbhandler(NTFY_PEERS)
68         past = int(time.time())-1000000000
69         peers = self.create_good_random_peers(btconn.current_version,num=200)
70         
71         peers = []
72         
73         for i in range(0,len(peers)):
74             peer = peers[i]
75             peer.update({'last_seen':past, 'last_connected':past})
76             del peer['connect_time']
77             peer['num_torrents'] = peer['nfiles'] 
78             del peer['nfiles']
79             commit = (i == len(peers)-1)
80             self.peerdb.addPeer(peer['permid'], peer, update_dns=True, update_connected=True, commit=commit)
81
82
83     def tearDown(self):
84         """ override TestAsServer """
85         TestAsServer.tearDown(self)
86         try:
87             os.remove(self.superpeerfilename)
88         except:
89             print_exc()
90
91
92     #Arno, 2010-02-16: we now kick v2 peers, so can't test anymore.
93     #def singtest_good_buddycast2(self):
94     #    self.subtest_good_buddycast(2)
95         
96     def singtest_good_buddycast3(self):
97         """ I want a fresh Tribler for this """
98         self.subtest_good_buddycast(3)
99         
100     def singtest_good_buddycast4(self):
101         """ I want a fresh Tribler for this """
102         self.subtest_good_buddycast(4)
103         
104     def singtest_good_buddycast6(self):
105         """ I want a fresh Tribler for this """
106         self.subtest_good_buddycast(6)
107
108     def singtest_bad_all(self):
109         """ 
110             I want to start a Tribler client once and then connect to
111             it many times. So there must be only one test method
112             to prevent setUp() from creating a new client every time.
113
114             The code is constructed so unittest will show the name of the
115             (sub)test where the error occured in the traceback it prints.
116         """
117         self.subtest_bad_not_bdecodable()
118         self.subtest_bad_not_dict1()
119         self.subtest_bad_not_dict2()
120         self.subtest_bad_empty_dict()
121         self.subtest_bad_wrong_dict_keys()
122         self.subtest_bad_buddycast_simple()
123         self.subtest_bad_taste_buddies()
124         self.subtest_bad_random_peers()
125
126     #
127     # Good BUDDYCAST
128     #
129     def subtest_good_buddycast(self,oversion):
130         """ 
131             test good BUDDYCAST messages
132         """
133         print >>sys.stderr,"test: good BUDDYCAST",oversion
134         s = OLConnection(self.my_keypair,'localhost',self.hisport,myoversion=oversion)
135         msg = self.create_good_buddycast_payload(oversion)
136         s.send(msg)
137
138         s.b.s.settimeout(60.0)
139         try:
140             while True:
141                 resp = s.recv()
142                 self.assert_(len(resp) > 0)
143                 print >>sys.stderr,"test: good BUDDYCAST: Got reply",getMessageName(resp[0])
144                 if resp[0] == BUDDYCAST:
145                     break
146                 elif resp[0] == GET_METADATA:
147                     self.check_get_metadata(resp[1:])
148                 elif resp[0] == KEEP_ALIVE:
149                     if oversion >= 3:
150                         self.check_keep_alive(resp[1:])
151                     else:
152                         print >> sys.stderr,"test: Tribler sent KEEP_ALIVE, not allowed in olproto ver",oversion
153                         self.assert_(False)
154         except socket.timeout:
155             print >> sys.stderr,"test: Timeout, bad, peer didn't reply with BUDDYCAST message"
156             self.assert_(False)
157
158         self.check_buddycast(resp[1:],oversion)
159         time.sleep(10)
160         # the other side should not have closed the connection, as
161         # this is all valid, so this should not throw an exception:
162         s.send('bla')
163         s.close()
164
165     def create_good_buddycast_payload(self,oversion):
166         d = self.create_good_buddycast(oversion)
167         return self.create_payload(d)
168         
169     def create_good_buddycast(self,oversion):
170         self.myprefs = self.create_good_my_prefs(oversion)
171         tastebuddies = self.create_good_taste_buddies(oversion)
172         randompeers = self.create_good_random_peers(oversion)
173         recentcoll = self.create_good_recently_collected_torrents(oversion)
174         d = {}
175         d['ip'] = '127.0.0.1'
176         d['port'] = 481
177         d['name'] = 'Bud Spencer'
178         d['preferences'] = self.myprefs
179         d['taste buddies'] = tastebuddies 
180         d['random peers'] = randompeers
181         
182         if oversion >= 3:
183             d['connectable'] = True
184         
185         if oversion >= 4:
186             d['collected torrents'] = recentcoll
187         
188         if oversion >= 6:
189             d['npeers'] = 3904
190             d['nfiles'] = 4027
191             d['ndls'] = 4553
192
193         #print >>sys.stderr,"test: Sending",`d`
194         
195         return d
196
197     def create_good_my_prefs(self,oversion,num=50):
198         p = []
199         for i in range(0,num):
200             infohash = chr(ord('a')+i) * 20
201             p.append(infohash)
202         return p
203
204     create_good_recently_collected_torrents = create_good_my_prefs
205
206     def create_good_taste_buddies(self,oversion):
207         tbs = []
208         for i in range(0,10):
209             tb = self.create_good_peer(i,oversion)
210             tbs.append(tb)
211         return tbs 
212
213     def create_good_random_peers(self,oversion,num=10):
214         tbs = []
215         for i in range(0,num):
216             tb = self.create_good_peer(i,oversion)
217             tbs.append(tb)
218         return tbs 
219         
220     def create_good_peer(self,id,oversion):
221         d = {}
222         d['permid'] = 'peer '+str(id)
223         d['ip'] = '192.168.0.'+str(id)
224         d['port'] = 7762+id
225         d['connect_time'] = int(time.time())
226
227         if oversion <= 2:
228             d['age'] = 0
229             
230         if oversion <= 3:
231             d['preferences'] = self.create_good_my_prefs(oversion,num=10)
232         else:
233             d['similarity'] = 1
234
235         if oversion >= 6:
236             d['oversion'] = btconn.current_version
237             d['nfiles'] = 100+id
238         
239         return d
240
241     def create_payload(self,r):
242         return BUDDYCAST+bencode(r)
243
244     def check_buddycast(self,data,oversion):
245         d = bdecode(data)
246         
247         print >>sys.stderr,"test: Got BUDDYCAST",d.keys()
248         #print >>sys.stderr,"test: Got CONTENT",`d`
249         
250         self.assert_(type(d) == DictType)
251         self.assert_('ip' in d)
252         self.assert_(type(d['ip']) == StringType)
253         self.assert_('port' in d)
254         self.assert_(type(d['port']) == IntType)
255         self.assert_('name' in d)
256         self.assert_(type(d['name']) == StringType)
257         self.assert_('preferences' in d)
258         self.check_preferences(d['preferences'],oversion)
259         self.assert_('taste buddies' in d)
260         self.check_taste_buddies(d['taste buddies'],oversion)
261         self.assert_('random peers' in d)
262         self.check_random_peers(d['random peers'],oversion
263                                 )
264         if oversion >= 3:
265             self.assert_('connectable' in d)
266             #print >>sys.stderr,"CONNECTABLE TYPE",type(d['connectable'])
267             self.assert_(type(d['connectable']) == IntType)
268         if oversion >= 4:
269             self.assert_('collected torrents' in d)
270             self.check_collected_torrents(d['collected torrents'],oversion)
271         if oversion >= 6:
272             self.assert_('npeers' in d)
273             self.assert_(type(d['npeers']) == IntType)
274             self.assert_('nfiles' in d)
275             self.assert_(type(d['nfiles']) == IntType)
276             self.assert_('ndls' in d)
277             self.assert_(type(d['ndls']) == IntType)
278
279     def check_preferences(self,p,oversion):
280         self.assert_(type(p) == ListType)
281         self.assert_(len(p) <= 50)
282         for infohash in p:
283             self.check_infohash(infohash)
284             
285     check_collected_torrents = check_preferences
286             
287     def check_infohash(self,infohash):
288         self.assert_(type(infohash) == StringType)
289         self.assert_(len(infohash) == 20)
290
291     def check_taste_buddies(self,peerlist,oversion):
292         return self.check_peer_list(peerlist,True,oversion)
293     
294     def check_random_peers(self,peerlist,oversion):
295         return self.check_peer_list(peerlist,False,oversion)
296
297     def check_peer_list(self,peerlist,taste,oversion):
298         self.assert_(type(peerlist) == ListType)
299         for p in peerlist:
300             self.check_peer(p,taste,oversion)
301
302     def check_peer(self,d,taste,oversion):
303         self.assert_(type(d) == DictType)
304         self.assert_('permid' in d)
305         self.assert_(type(d['permid']) == StringType)
306         self.assert_('ip' in d)
307         self.assert_(type(d['ip']) == StringType)
308         self.assert_('port' in d)
309         self.assert_(type(d['port']) == IntType)
310         self.assert_('connect_time' in d)
311         self.assert_(type(d['connect_time']) == IntType)
312
313         if oversion <= 3 and taste:
314             self.assert_('preferences' in d)
315             self.check_preferences(d['preferences'],oversion)
316         
317         if oversion >= 4:
318             self.assert_('similarity' in d)
319             self.assert_(type(d['similarity']) == IntType)
320
321         if oversion >= 6:
322             if 'oversion' in d:
323                 # Jie made this optional, only if peer has enough collected files
324                 # its record will contain these fields
325                 self.assert_(type(d['oversion']) == IntType)
326                 self.assert_('nfiles' in d)
327                 self.assert_(type(d['nfiles']) == IntType)
328             
329
330     def check_get_metadata(self,data):
331         infohash = bdecode(data)
332         self.check_infohash(infohash)
333         
334         # Extra check: he can only ask us for metadata for an infohash we
335         # gave him.
336         self.assert_(infohash in self.myprefs)        
337
338     def check_keep_alive(self,data):
339         self.assert_(len(data) == 0)
340
341     #
342     # Bad buddycast
343     #    
344     def subtest_bad_not_bdecodable(self):
345         self._test_bad(self.create_not_bdecodable)
346
347     def subtest_bad_not_dict1(self):
348         self._test_bad(self.create_not_dict1)
349
350     def subtest_bad_not_dict2(self):
351         self._test_bad(self.create_not_dict2)
352
353     def subtest_bad_empty_dict(self):
354         self._test_bad(self.create_empty_dict)
355
356     def subtest_bad_wrong_dict_keys(self):
357         self._test_bad(self.create_wrong_dict_keys)
358
359     def subtest_bad_buddycast_simple(self):
360         methods = [
361             self.make_bad_ip,
362             self.make_bad_port,
363             self.make_bad_name,
364             self.make_bad_preferences,
365             self.make_bad_collected_torrents]
366         for method in methods:
367             print >> sys.stderr,"\ntest: ",method,
368             self._test_bad(method)
369         
370         
371     def make_bad_ip(self):
372         d = self.create_good_buddycast(btconn.current_version)
373         d['ip'] = 481
374         return self.create_payload(d)
375
376     def make_bad_port(self):
377         d = self.create_good_buddycast(btconn.current_version)
378         d['port'] = '127.0.0.1'
379         return self.create_payload(d)
380
381     def make_bad_name(self):
382         d = self.create_good_buddycast(btconn.current_version)
383         d['name'] = 481
384         return self.create_payload(d)
385     
386     def make_bad_preferences(self):
387         d = self.create_good_buddycast(btconn.current_version)
388         d['preferences'] = 481
389         return self.create_payload(d)
390
391     def make_bad_collected_torrents(self):
392         d = self.create_good_buddycast(btconn.current_version)
393         d['collected torrents'] = 481
394         return self.create_payload(d)
395
396         
397     def subtest_bad_taste_buddies(self):
398         methods = [
399             self.make_bad_tb_not_list,
400             self.make_bad_tb_list_not_dictelems,
401             self.make_bad_tb_list_bad_peer]
402         for method in methods:
403             d = self.create_good_buddycast(btconn.current_version)
404             d['taste buddies'] = method()
405             func = lambda:self.create_payload(d)
406             
407             print >> sys.stderr,"\ntest: ",method,
408             self._test_bad(func)
409
410     def make_bad_tb_not_list(self):
411         tbs = 481
412         return tbs
413         
414     def make_bad_tb_list_not_dictelems(self):
415         tbs = []
416         for i in range(0,50):
417             tbs.append(i)
418         return tbs
419         
420     def make_bad_tb_list_bad_peer(self):
421         tbs = []
422         for i in range(0,50):
423             tbs.append(self.make_bad_peer())
424         return tbs
425
426     def make_bad_peer(self):
427         d = {}
428         d['permid'] = 'peer 481'
429         # Error is too little fields. 
430         # TODO: test all possible bad peers
431         
432         return d
433
434
435     def subtest_bad_random_peers(self):
436         methods = [
437             self.make_bad_ip,
438             self.make_bad_port,
439             self.make_bad_name,
440             self.make_bad_preferences,
441             self.make_bad_collected_torrents]
442         for method in methods:
443             d = self.create_good_buddycast(btconn.current_version)
444             d['taste buddies'] = method()
445             func = lambda:self.create_payload(d)
446             
447             print >> sys.stderr,"\ntest: ",method,
448             self._test_bad(func)
449     
450     def _test_bad(self,gen_buddycast_func):
451         print >>sys.stderr,"test: bad BUDDYCAST",gen_buddycast_func
452         s = OLConnection(self.my_keypair,'localhost',self.hisport)
453         msg = gen_buddycast_func()
454         s.send(msg)
455         time.sleep(5)
456         # the other side should not like this and close the connection
457         self.assert_(len(s.recv())==0)
458         s.close()
459
460     def create_not_bdecodable(self):
461         return BUDDYCAST+"bla"
462
463     def create_not_dict1(self):
464         buddycast = 481
465         return self.create_payload(buddycast)
466
467     def create_not_dict2(self):
468         buddycast = []
469         return self.create_payload(buddycast)
470
471     def create_empty_dict(self):
472         buddycast = {}
473         return self.create_payload(buddycast)
474
475     def create_wrong_dict_keys(self):
476         buddycast = {}
477         buddycast['bla1'] = '\x00\x00\x00\x00\x00\x30\x00\x00'
478         buddycast['bla2'] = '\x00\x00\x00\x00\x00\x30\x00\x00'
479         return self.create_payload(buddycast)
480
481 def test_suite():
482     suite = unittest.TestSuite()
483     # We should run the tests in a separate Python interpreter to prevent 
484     # problems with our singleton classes, e.g. PeerDB, etc.
485     if len(sys.argv) != 2:
486         print "Usage: python test_buddycast_msg.py <method name>"
487     else:
488         suite.addTest(TestBuddyCastMsg(sys.argv[1]))
489     
490     return suite
491
492 def main():
493     unittest.main(defaultTest='test_suite',argv=[sys.argv[0]])
494
495 if __name__ == "__main__":
496     main()