1 # Written by Arno Bakker, George Milescu
2 # see LICENSE.txt for license information
4 # Like test_secure_overlay, we start a new python interpreter for each test.
5 # Although we don't have the singleton problem here, we do need to do this as the
6 # HTTPServer that MyTracker uses won't relinquish the listen socket, causing
7 # "address in use" errors in the next test. This is probably due to the fact that
8 # MyTracker has a thread mixed in, as a listensocket.close() normally releases it
17 from types import ListType
24 from BaseLib.Test.test_as_server import TestAsServer
25 from btconn import BTConnection
26 from olconn import OLConnection
27 from BaseLib.Core.RequestPolicy import AllowAllRequestPolicy
28 from BaseLib.Core.BitTornado.bencode import bencode,bdecode
29 from BaseLib.Core.BitTornado.bitfield import Bitfield
30 from BaseLib.Core.BitTornado.BT1.MessageID import *
31 from BaseLib.Core.BitTornado.BT1.convert import toint
32 from BaseLib.Core.CacheDB.CacheDBHandler import FriendDBHandler, TorrentDBHandler
33 from BaseLib.Test.test_connect_overlay import MyTracker
37 class TestProxyServiceAsCoordinator(TestAsServer):
38 """ This class tests the ProxyService Helper stack. It simulates a coordinator and connects to the
39 helper instance, sending messages to it and verifying the received responses
43 """ override TestAsServer """
44 TestAsServer.setUp(self)
45 print >>sys.stderr,"test: Giving MyLaunchMany time to startup"
47 print >>sys.stderr,"test: MyLaunchMany should have started up"
49 def setUpPreSession(self):
50 """ override TestAsServer """
51 TestAsServer.setUpPreSession(self)
53 self.setUpMyListenSockets()
55 # Must be changed in test/extend_hs_dir/proxyservice.test.torrent as well
56 self.mytrackerport = 4901
57 self.myid = 'R410-----HgUyPu56789'
58 self.mytracker = MyTracker(self.mytrackerport,self.myid,'127.0.0.1',self.mylistenport)
59 self.mytracker.background_serve()
61 self.myid2 = 'R410-----56789HuGyx0' # used for the coordinator
63 # Arno, 2009-12-15: Make sure coop downloads have their own destdir
64 destdir = tempfile.mkdtemp()
65 self.config.set_download_help_dir(destdir)
67 # Set the proxyservice to full speed
68 self.config.set_proxyservice_status(1) #PROXYSERVICE_ON=1
70 def setUpMyListenSockets(self):
71 # Start our server side, to which Tribler will try to connect
72 # coordinator BitTorrent socket (the helper connects to this socket to sent BT messages with pieces requested by the coordinator)
73 self.mylistenport = 4810
74 self.myss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
75 self.myss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
76 self.myss.bind(('', self.mylistenport))
79 # Leecher socket (the helper connects to this socket to download the pieces requested by the coordinator)
80 self.mylistenport2 = 3726
81 self.myss2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
82 self.myss2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
83 self.myss2.bind(('', self.mylistenport2))
86 def setUpPostSession(self):
87 """ override TestAsServer """
88 TestAsServer.setUpPostSession(self)
90 self.mypermid = str(self.my_keypair.pub().get_der())
91 self.hispermid = str(self.his_keypair.pub().get_der())
93 # Calculating the infohash for proxyservice.test.torrent
94 self.torrentfile = os.path.join('extend_hs_dir','proxyservice.test.torrent')
96 # Read torrent file to calculate the infohash
97 torrentfile_content = open(self.torrentfile, "rb")
99 metainfo = bdecode(torrentfile_content.read())
100 # Calculate the torrent length
101 if "length" in metainfo["info"]:
102 self.length = metainfo["info"]["length"]
105 for f in metainfo["info"]["files"]:
106 self.length += f["length"]
107 # Re-encode only the info section
108 self.infohash = hashlib.sha1(bencode(metainfo['info'])).digest()
109 # Store the number of pieces
110 self.numpieces = int(math.ceil((self.length / metainfo["info"]["piece length"])))
111 # Close the torrentfile
112 torrentfile_content.close()
114 # Add us as friend, so he will accept the ASK_FOR_HELP message
115 if False: # TEMP: friendsdb doesn't have an addFriend method
116 # friendsdb = FriendDBHandler.getInstance()
117 # friendsdb.addFriend(self.mypermid)
120 # Accept overlay requests from anybody
121 self.session.set_overlay_request_policy(AllowAllRequestPolicy())
123 self.session.set_download_states_callback(self.states_callback)
125 statedir=self.session.get_state_dir()
126 os.system('cp /tmp/Gopher.torrent ' + statedir + '/collected_torrent_files/Gopher.torrent')
130 """ override TestAsServer """
131 print >> sys.stderr,"test: *** TEARDOWN"
132 TestAsServer.tearDown(self)
133 self.mytracker.shutdown()
134 self.tearDownMyListenSockets()
137 def tearDownMyListenSockets(self):
142 def states_callback(self,dslist):
143 print >>sys.stderr,"stats: dslist",len(dslist)
145 print >>sys.stderr,"stats: coordinator",`ds.get_coopdl_coordinator()`
146 print >>sys.stderr,"stats: helpers",`ds.get_coopdl_helpers()`
147 print >>sys.stderr, ""
150 # Creates dictionary with the correct (good) commands used by the coordinator to test the helper
151 def get_genresdict(self):
153 genresdict[ASK_FOR_HELP] = (self.create_good_ask_for_help,True)
154 genresdict[STOP_HELPING] = (self.create_good_stop_helping,True)
155 genresdict[REQUEST_PIECES] = (self.create_good_request_pieces,True)
156 genresdict[CANCEL_PIECE] = (self.create_good_cancel_piece,True)
157 # The helper will request the .torrent metadata
158 genresdict[METADATA] = (self.create_good_metadata,True)
163 # Good proxy messages
165 def singtest_good_proxy(self):
166 genresdict = self.get_genresdict()
167 print >> sys.stderr, "test: good ASK_FOR_HELP"
168 self._test_proxy(genresdict)
174 def singtest_bad_proxy_ask_for_help(self):
175 # Get the correct messages used by the coordinator
176 genresdict = self.get_genresdict()
177 # Prepare a bad ASK_FOR_HELP message
178 genresdict[ASK_FOR_HELP] = (self.create_bad_ask_for_help_no_infohash,False)
179 print >> sys.stderr, "test: bad ask_for_help"
180 self._test_proxy(genresdict)
182 def singtest_bad_proxy_metadata_not_bdecodable(self):
183 # Get the correct messages used by the coordinator
184 genresdict = self.get_genresdict()
185 # Prepare a bad METADATA message
186 genresdict[METADATA] = (self.create_bad_metadata_not_bdecodable,False)
187 print >> sys.stderr, "test: bad METADATA (not bdecodable)", genresdict[METADATA][0]
188 self._test_proxy(genresdict)
190 def singtest_bad_proxy_metadata_not_dict1(self):
191 # Get the correct messages used by the coordinator
192 genresdict = self.get_genresdict()
193 # Prepare a bad METADATA message
194 genresdict[METADATA] = (self.create_bad_metadata_not_dict1,False)
195 print >> sys.stderr, "test: bad METADATA (not a dictionary)", genresdict[METADATA][0]
196 self._test_proxy(genresdict)
198 def singtest_bad_proxy_metadata_not_dict2(self):
199 # Get the correct messages used by the coordinator
200 genresdict = self.get_genresdict()
201 # Prepare a bad METADATA message
202 genresdict[METADATA] = (self.create_bad_metadata_not_dict2,False)
203 print >>sys.stderr,"test: bad METADATA (not a dictionary)",genresdict[METADATA][0]
204 self._test_proxy(genresdict)
206 def singtest_bad_2fast_metadata_empty_dict(self):
207 # Get the correct messages used by the coordinator
208 genresdict = self.get_genresdict()
209 # Prepare a bad METADATA message
210 genresdict[METADATA] = (self.create_bad_metadata_empty_dict,False)
211 print >>sys.stderr,"test: bad METADATA (empty dictionary)",genresdict[METADATA][0]
212 self._test_proxy(genresdict)
214 def singtest_bad_proxy_metadata_wrong_dict_keys(self):
215 # Get the correct messages used by the coordinator
216 genresdict = self.get_genresdict()
217 # Prepare a bad METADATA message
218 genresdict[METADATA] = (self.create_bad_metadata_wrong_dict_keys,False)
219 print >>sys.stderr,"test: bad METADATA (wrong keys in dictionary)",genresdict[METADATA][0]
220 self._test_proxy(genresdict)
222 def singtest_bad_proxy_metadata_bad_torrent1(self):
223 # Get the correct messages used by the coordinator
224 genresdict = self.get_genresdict()
225 # Prepare a bad METADATA message
226 genresdict[METADATA] = (self.create_bad_metadata_bad_torrent1,False)
227 print >>sys.stderr,"test: bad METADATA (wrong metadata field in dictionary)",genresdict[METADATA][0]
228 self._test_proxy(genresdict)
231 def singtest_bad_proxy_metadata_bad_torrent2(self):
232 # Get the correct messages used by the coordinator
233 genresdict = self.get_genresdict()
234 # Prepare a bad METADATA message
235 genresdict[METADATA] = (self.create_bad_metadata_bad_torrent2,False)
236 print >>sys.stderr,"test: bad METADATA (empty dictionary in metadata filed)",genresdict[METADATA][0]
237 self._test_proxy(genresdict)
239 def singtest_bad_proxy_metadata_bad_torrent3(self):
240 # Get the correct messages used by the coordinator
241 genresdict = self.get_genresdict()
242 # Prepare a bad METADATA message
243 genresdict[METADATA] = (self.create_bad_metadata_bad_torrent3,False)
244 print >>sys.stderr,"test: bad METADATA (bad metadata field in dictionary)",genresdict[METADATA][0]
245 self._test_proxy(genresdict)
249 def _test_proxy(self,genresdict):
250 """ Send messages to the helper instance and test it.
252 Testing ASK_FOR_HELP, STOP_HELPING, REQUEST_PIECES, CANCEL_PIECE and METADATA
254 # 1. Establish overlay connection to Tribler
255 ol_connection = OLConnection(self.my_keypair, 'localhost', self.hisport, mylistenport=self.mylistenport2)
257 # 2. Send the ASK_FOR_HELP message
258 (generate_data,sent_good_values) = genresdict[ASK_FOR_HELP]
259 msg = generate_data()
260 ol_connection.send(msg)
262 # Read the helper's response
263 resp = ol_connection.recv()
264 # Check the helper's response
265 # 3. At this point, the helper does not have the .torrent file, so it requests it with a METADATA message
266 self.assert_(resp[0] == GET_METADATA)
267 self.check_get_metadata(resp[1:])
268 print >>sys.stderr,"test: Got GET_METADATA for torrent, good"
270 # Read the helper's response
271 resp = ol_connection.recv()
272 # Check the helper's response
273 self.assert_(len(resp)==0)
274 ol_connection.close()
278 (generate_data,sent_good_values) = genresdict[METADATA]
279 msg = generate_data()
280 ol_connection.send(msg)
282 # 5. At this point the helper is confirming his availability to help
283 # Read the helper's response
284 resp = ol_connection.recv()
285 # Check the helper's response
286 self.assert_(resp[0] == JOIN_HELPERS)
287 self.check_ask_for_help(resp)
288 print >>sys.stderr,"test: Got JOIN_HELPERS for torrent, good"
290 # 6. At this point, the helper will contact the tracker and then wait for REQUEST_PIECES messages
291 # So we send a request pieces message
292 (generate_data,sent_good_values) = genresdict[REQUEST_PIECES]
293 msg = generate_data()
294 ol_connection.send(msg)
296 # At this point the helper will contact the seeders in the swarm to download the requested piece
297 # There is only one seeder in the swarm, the coordinator's twin
298 # 8. Our tracker says there is another peer (also us) on port 4810
299 # Now accept a connection on that port and pretend we're a seeder
300 self.myss.settimeout(10.0)
301 conn, addr = self.myss.accept()
302 options = '\x00\x00\x00\x00\x00\x00\x00\x00'
303 s2 = BTConnection('',0,conn,user_option_pattern=options,user_infohash=self.infohash,myid=self.myid)
304 s2.read_handshake_medium_rare()
306 # Send a bitfield message to the helper (pretending we are a regular seeder)
307 b = Bitfield(self.numpieces)
308 for i in range(self.numpieces):
310 self.assert_(b.complete())
311 msg = BITFIELD+b.tostring()
315 print >>sys.stderr,"test: Got BT connection to us, as fake seeder, good"
317 resp = ol_connection.recv()
318 self.assert_(len(resp)==0)
319 ol_connection.close()
322 # 7. Accept the data connection the helper wants to establish with us, the coordinator.
323 # The helper will send via this connection the pieces we request it to download.
324 self.myss2.settimeout(10.0)
325 conn, addr = self.myss2.accept()
326 s3 = BTConnection('',0,conn,user_infohash=self.infohash,myid=self.myid2)
327 s3.read_handshake_medium_rare()
331 print >>sys.stderr,"test: Got data connection to us, as coordinator, good"
333 # 9. At this point the helper should sent a PROXY_HAVE message on the overlay connection
334 # resp = ol_connection.recv()
335 # self.assert_(resp[0] == PROXY_HAVE)
336 # print >>sys.stderr,"test: Got PROXY)HAVE, good"
338 # 10. Await REQUEST on fake seeder
341 s2.s.settimeout(10.0)
343 self.assert_(len(resp) > 0)
344 print "test: Fake seeder got message",getMessageName(resp[0])
345 if resp[0] == REQUEST:
346 self.check_request(resp[1:])
347 print >>sys.stderr,"test: Fake seeder got REQUEST for reserved piece, good"
350 except socket.timeout:
351 print >> sys.stderr,"test: Timeout, bad, fake seeder didn't reply with message"
354 # 11. Sent the helper a STOP_HELPING message
355 (generate_data,sent_good_values) = genresdict[STOP_HELPING]
356 msg = generate_data()
357 ol_connection.send(msg)
358 # The other side should close the connection, whether the msg was good or bad
359 resp = ol_connection.recv()
360 self.assert_(len(resp)==0)
361 ol_connection.close()
363 def check_request(self,data):
364 piece = toint(data[0:4])
365 self.assert_(piece == 1)
368 # Correct (good) messages used by the coordinator to test the helper
370 def create_good_ask_for_help(self):
371 """ Create a correctly formatted ASK_FOR_HELP message and return it
373 # Generate a random challenge - random number on 8 bytes (62**8 possible combinations)
374 chars = string.letters + string.digits #len(chars)=62
377 challenge = challenge + random.choice(chars)
379 return ASK_FOR_HELP + self.infohash + bencode(challenge)
381 def check_ask_for_help(self, data):
382 """ Check the answer the coordinator got for an ASK_FOR_HELP message
383 The helper should have sent a JOIN_HELPERS message
385 infohash = data[1:21]
386 self.assert_(infohash == self.infohash)
390 def create_good_stop_helping(self):
391 return STOP_HELPING + self.infohash
393 def check_stop_helping(self, data):
394 """ Check the answer the coordinator got for a STOP_HELPING message
395 The helper should have sent a RESIGN_AS_HELPER message
397 infohash = data[1:21]
398 self.assert_(infohash == self.infohash)
402 def create_good_request_pieces(self):
403 # Request piece number 1
405 return REQUEST_PIECES + self.infohash + bencode(piece)
406 # The reply for this message is a BT Have message
410 def create_good_cancel_piece(self):
411 # Cancel piece number 1
413 return CANCEL_PIECE + self.infohash + bencode(piece)
414 # This message is not supposed to have any reply
415 # TODO: test the DROPEPD_PIECE message, after implementation
419 def create_good_metadata(self):
420 f = open(self.torrentfile,"rb")
424 d = self.create_good_metadata_dict(data)
428 def create_good_metadata_dict(self,data):
430 d['torrent_hash'] = self.infohash
434 d['last_check_time'] = int(time.time())
438 def check_get_metadata(self,data):
439 infohash = bdecode(data) # is bencoded for unknown reason, can't change it =))
440 self.assert_(infohash == self.infohash)
445 # Incorrect (bad) messages used by the coordinator to test the helper
447 def create_bad_ask_for_help_no_infohash(self):
448 return ASK_FOR_HELP+"481"
450 def create_bad_metadata_not_bdecodable(self):
451 return METADATA+"bla"
453 def create_bad_metadata_not_dict1(self):
455 return METADATA+bencode(d)
457 def create_bad_metadata_not_dict2(self):
459 return METADATA+bencode(d)
461 def create_bad_metadata_empty_dict(self):
463 return METADATA+bencode(d)
465 def create_bad_metadata_wrong_dict_keys(self):
467 d['bla1'] = '\x00\x00\x00\x00\x00\x30\x00\x00'
468 d['bla2'] = '\x00\x00\x00\x00\x00\x30\x00\x00'
469 return METADATA+bencode(d)
471 def create_bad_metadata_bad_torrent1(self):
472 d = self.create_good_metadata_dict(None)
473 d['metadata'] = '\x12\x34' * 100 # random data
477 def create_bad_metadata_bad_torrent2(self):
479 data = bencode(torrent)
481 d = self.create_good_metadata_dict(data)
487 def create_bad_metadata_bad_torrent3(self):
488 torrent = {'info':481}
489 data = bencode(torrent)
491 d = self.create_good_metadata_dict(data)
499 suite = unittest.TestSuite()
500 # We should run the tests in a separate Python interpreter to prevent
501 # problems with our singleton classes, e.g. PeerDB, etc.
502 if len(sys.argv) != 2:
503 print "Usage: python test_proxyservice_as_coord.py <method name>"
505 suite.addTest(TestProxyServiceAsCoordinator(sys.argv[1]))
508 print "*** Calling TestProxyServiceAsCoordinator with argument " + sys.argv[1]
514 unittest.main(defaultTest='test_suite',argv=[sys.argv[0]])
516 if __name__ == "__main__":