package com.limegroup.gnutella; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.Socket; import java.util.Arrays; import java.util.Iterator; import java.util.Random; import junit.framework.Test; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.messages.vendor.LimeACKVendorMessage; import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage; import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage; import com.limegroup.gnutella.messages.vendor.UDPConnectBackVendorMessage; import com.limegroup.gnutella.routing.QueryRouteTable; import com.limegroup.gnutella.routing.RouteTableMessage; import com.limegroup.gnutella.stubs.ActivityCallbackStub; import com.limegroup.gnutella.util.PrivilegedAccessor; /** * Tests that an Ultrapeer correctly handles out-of-band queries. Essentially * tests the following methods of MessageRouter: handleQueryRequest, * handleUDPMessage, handleLimeAckMessage, Expirer, QueryBundle, sendQueryReply * * ULTRAPEER_1 ---- CENTRAL TEST ULTRAPEER ---- ULTRAPEER[1] * | * | * | * LEAF[0] * * This test should cover the case for leaves too, since there is no difference * between Leaf and UP when it comes to this behavior. */ public final class ServerSideOutOfBandReplyTest extends ServerSideTestCase { protected static int TIMEOUT = 2000; /** * Ultrapeer 1 UDP connection. */ private static DatagramSocket UDP_ACCESS; public ServerSideOutOfBandReplyTest(String name) { super(name); } public static Test suite() { return buildTestSuite(ServerSideOutOfBandReplyTest.class); } public static void main(String[] args) { junit.textui.TestRunner.run(suite()); } public static Integer numUPs() { return new Integer(3); } public static Integer numLeaves() { return new Integer(1); } public static ActivityCallback getActivityCallback() { return new ActivityCallbackStub(); } public static void setUpQRPTables() throws Exception { QueryRouteTable qrt = new QueryRouteTable(); qrt.add("berkeley"); qrt.add("susheel"); qrt.addIndivisible(HugeTestUtils.UNIQUE_SHA1.toString()); for (Iterator iter=qrt.encode(null).iterator(); iter.hasNext(); ) { LEAF[0].send((RouteTableMessage)iter.next()); LEAF[0].flush(); } // for Ultrapeer 1 qrt = new QueryRouteTable(); qrt.add("leehsus"); qrt.add("berkeley"); for (Iterator iter=qrt.encode(null).iterator(); iter.hasNext(); ) { ULTRAPEER[0].send((RouteTableMessage)iter.next()); ULTRAPEER[0].flush(); } } // BEGIN TESTS // ------------------------------------------------------ // tests basic out of band functionality // this tests solicited UDP support - it should participate in ACK exchange public void testBasicOutOfBandRequest() throws Exception { PrivilegedAccessor.setValue( RouterService.getUdpService(), "_acceptedSolicitedIncoming", new Boolean(true)); PrivilegedAccessor.setValue( RouterService.getUdpService(), "_acceptedUnsolicitedIncoming", new Boolean(true)); DatagramPacket pack = null; UDP_ACCESS = new DatagramSocket(); drainAll(); QueryRequest query = QueryRequest.createOutOfBandQuery("txt", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); // we needed to hop the message because we need to make it seem that it // is from sufficiently far away.... ULTRAPEER[0].send(query); ULTRAPEER[0].flush(); // we should get a ReplyNumberVendorMessage via UDP - we'll get an // interrupted exception if not Message message = null; while (!(message instanceof ReplyNumberVendorMessage)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get VM", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); // we should NOT get a reply to our query assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), query.getGUID())))); } // make sure the GUID is correct assertTrue(Arrays.equals(query.getGUID(), message.getGUID())); ReplyNumberVendorMessage reply = (ReplyNumberVendorMessage) message; assertEquals(2, reply.getNumResults()); assertTrue(reply.canReceiveUnsolicited()); //rince and repeat, this time pretend to be firewalled query = QueryRequest.createOutOfBandQuery("txt", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); UDPService service = RouterService.getUdpService(); PrivilegedAccessor.setValue( service,"_acceptedUnsolicitedIncoming",new Boolean(false)); assertFalse(RouterService.getUdpService().canReceiveUnsolicited()); assertTrue(query.desiresOutOfBandReplies()); // we needed to hop the message because we need to make it seem that it // is from sufficiently far away.... ULTRAPEER[1].send(query); ULTRAPEER[1].flush(); // we should get a ReplyNumberVendorMessage via UDP - we'll get an // interrupted exception if not message = null; while (!(message instanceof ReplyNumberVendorMessage)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get VM", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); // we should NOT get a reply to our query assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), query.getGUID())))); } // make sure the GUID is correct assertTrue(Arrays.equals(query.getGUID(), message.getGUID())); reply = (ReplyNumberVendorMessage) message; assertEquals(2, reply.getNumResults()); assertFalse(reply.canReceiveUnsolicited()); //restore our un-firewalled status and repeat query = QueryRequest.createOutOfBandQuery("txt", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); PrivilegedAccessor.setValue( service,"_acceptedUnsolicitedIncoming",new Boolean(true)); ULTRAPEER[2].send(query); ULTRAPEER[2].flush(); // we should get a ReplyNumberVendorMessage via UDP - we'll get an // interrupted exception if not message = null; while (!(message instanceof ReplyNumberVendorMessage)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get VM", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); // we should NOT get a reply to our query assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), query.getGUID())))); } // make sure the GUID is correct assertTrue(Arrays.equals(query.getGUID(), message.getGUID())); reply = (ReplyNumberVendorMessage) message; assertEquals(2, reply.getNumResults()); assertTrue(reply.canReceiveUnsolicited()); // ok - we should ACK the ReplyNumberVM ByteArrayOutputStream baos = new ByteArrayOutputStream(); LimeACKVendorMessage ack = new LimeACKVendorMessage(new GUID(message.getGUID()), reply.getNumResults()); ack.write(baos); pack = new DatagramPacket(baos.toByteArray(), baos.toByteArray().length, pack.getAddress(), pack.getPort()); UDP_ACCESS.send(pack); // now we should get TWO replies, each with one response //1) while (!(message instanceof QueryReply)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get reply", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); // as long as we don't get a ClassCastException we are good to go message = Message.read(in); } // make sure this is the correct QR assertTrue(Arrays.equals(message.getGUID(), ack.getGUID())); assertEquals(1, ((QueryReply)message).getResultCount()); //2) null out 'message' so we can get the next reply.... message = null; while (!(message instanceof QueryReply)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get reply", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); // as long as we don't get a ClassCastException we are good to go message = Message.read(in); } // make sure this is the correct QR assertTrue(Arrays.equals(message.getGUID(), ack.getGUID())); assertEquals(1, ((QueryReply)message).getResultCount()); // make sure that if we send the ACK we don't get another reply - this // is current policy but we may want to change it in the future baos = new ByteArrayOutputStream(); ack.write(baos); pack = new DatagramPacket(baos.toByteArray(), baos.toByteArray().length, pack.getAddress(), pack.getPort()); UDP_ACCESS.send(pack); // now we should NOT get the reply! try { while (true) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); UDP_ACCESS.receive(pack); InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), ack.getGUID())))); } } catch (IOException expected) {} } // makes sure that if we ack back with less results than what the server // has we only get back what we asked for. public void testPiecemealOutOfBandResults() throws Exception { DatagramPacket pack = null; drainAll(); QueryRequest query = QueryRequest.createOutOfBandQuery("txt", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); // we needed to hop the message because we need to make it seem that it // is from sufficiently far away.... ULTRAPEER[0].send(query); ULTRAPEER[0].flush(); // we should get a ReplyNumberVendorMessage via UDP - we'll get an // interrupted exception if not Message message = null; while (!(message instanceof ReplyNumberVendorMessage)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get VM", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); // we should NOT get a reply to our query assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), query.getGUID())))); } // make sure the GUID is correct assertTrue(Arrays.equals(query.getGUID(), message.getGUID())); ReplyNumberVendorMessage reply = (ReplyNumberVendorMessage) message; assertEquals(2, reply.getNumResults()); // ok - we should ACK the ReplyNumberVM ByteArrayOutputStream baos = new ByteArrayOutputStream(); LimeACKVendorMessage ack = new LimeACKVendorMessage(new GUID(message.getGUID()), 1); ack.write(baos); pack = new DatagramPacket(baos.toByteArray(), baos.toByteArray().length, pack.getAddress(), pack.getPort()); UDP_ACCESS.send(pack); // now we should get the reply! while (!(message instanceof QueryReply)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get reply", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); // as long as we don't get a ClassCastException we are good to go message = Message.read(in); } // make sure this is the correct QR assertTrue(Arrays.equals(message.getGUID(), ack.getGUID())); assertEquals(1, ((QueryReply)message).getResultCount()); // make sure that if we send the ACK we don't get another reply - this // is current policy but we may want to change it in the future baos = new ByteArrayOutputStream(); ack.write(baos); pack = new DatagramPacket(baos.toByteArray(), baos.toByteArray().length, pack.getAddress(), pack.getPort()); UDP_ACCESS.send(pack); // now we should NOT get the reply! try { while (true) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); UDP_ACCESS.receive(pack); InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), ack.getGUID())))); } } catch (IOException expected) {} } // makes sure that if we ACK back with a 0 for the number of results we // don't get a query reply public void testSimpleACKOutOfBandResults() throws Exception { DatagramPacket pack = null; drainAll(); QueryRequest query = QueryRequest.createOutOfBandQuery("txt", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); // we needed to hop the message because we need to make it seem that it // is from sufficiently far away.... ULTRAPEER[0].send(query); ULTRAPEER[0].flush(); // we should get a ReplyNumberVendorMessage via UDP - we'll get an // interrupted exception if not Message message = null; while (!(message instanceof ReplyNumberVendorMessage)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get VM", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); // we should NOT get a reply to our query assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), query.getGUID())))); } // make sure the GUID is correct assertTrue(Arrays.equals(query.getGUID(), message.getGUID())); ReplyNumberVendorMessage reply = (ReplyNumberVendorMessage) message; assertEquals(2, reply.getNumResults()); // ok - we should ACK the ReplyNumberVM ByteArrayOutputStream baos = new ByteArrayOutputStream(); LimeACKVendorMessage ack = new LimeACKVendorMessage(new GUID(message.getGUID()), 0); ack.write(baos); pack = new DatagramPacket(baos.toByteArray(), baos.toByteArray().length, pack.getAddress(), pack.getPort()); UDP_ACCESS.send(pack); // now we should NOT get the reply! try { while (true) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); UDP_ACCESS.receive(pack); InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); assertTrue(!((message instanceof QueryReply) && (Arrays.equals(message.getGUID(), ack.getGUID())))); } } catch (IOException expected) {} } // tests that MessageRouter expires GUIDBundles/Replies in a timely fashion // this test requires solicited support public void testExpirer() throws Exception { drainAll(); DatagramPacket pack = null; // THIS TESTS ASSUMES SOLICITED SUPPORT - set up in a previous test QueryRequest query = QueryRequest.createOutOfBandQuery("berkeley", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); // we needed to hop the message because we need to make it seem that it // is from sufficiently far away.... ULTRAPEER[1].send(query); ULTRAPEER[1].flush(); // we should get a ReplyNumberVendorMessage via UDP - we'll get an // interrupted exception if not Message message = null; while (!(message instanceof ReplyNumberVendorMessage)) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); try { UDP_ACCESS.receive(pack); } catch (IOException bad) { fail("Did not get VM", bad); } InputStream in = new ByteArrayInputStream(pack.getData()); // as long as we don't get a ClassCastException we are good to go message = Message.read(in); } // make sure the GUID is correct assertTrue(Arrays.equals(query.getGUID(), message.getGUID())); ReplyNumberVendorMessage reply = (ReplyNumberVendorMessage) message; assertEquals(1, reply.getNumResults()); // WAIT for the expirer to expire the query reply Thread.sleep(60 * 1000); // 1 minute - expirer must run twice // ok - we should ACK the ReplyNumberVM and NOT get a reply ByteArrayOutputStream baos = new ByteArrayOutputStream(); LimeACKVendorMessage ack = new LimeACKVendorMessage(new GUID(message.getGUID()), reply.getNumResults()); ack.write(baos); pack = new DatagramPacket(baos.toByteArray(), baos.toByteArray().length, pack.getAddress(), pack.getPort()); UDP_ACCESS.send(pack); // now we should NOT get the reply! keep reading until buffer is empty try { while (true) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); UDP_ACCESS.receive(pack); InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); assertTrue(!((message instanceof QueryReply) && (Arrays.equals(ack.getGUID(), message.getGUID())))); } } catch (IOException expected) {} // good - now lets test that if we send a LOT of out-of-band queries, // we get a lot of ReplyNumberVMs but at the 251st we don't get a // ReplyNumberVM - this test may be fragile because i'm hardcoding // MAX_BUFFERED_REPLIES from MessageRouter final int MAX_BUFFERED_REPLIES = 15; // ok, we need to set MAX_BUFFERED_REPLIES in MessageRouter MessageRouter.MAX_BUFFERED_REPLIES = MAX_BUFFERED_REPLIES; // send 15 queries Random rand = new Random(); int numReplyNumberVMs = 0; for (int i = 0; i < MAX_BUFFERED_REPLIES; i++) { query = QueryRequest.createOutOfBandQuery((i%2==0) ? "berkeley" : "susheel", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); if (rand.nextInt(2) == 0) { ULTRAPEER[1].send(query); ULTRAPEER[1].flush(); } else { ULTRAPEER[0].send(query); ULTRAPEER[0].flush(); } Thread.sleep(1250); // count 15 ReplyNumberVMs try { while (true) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); UDP_ACCESS.receive(pack); InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); if (message instanceof ReplyNumberVendorMessage) numReplyNumberVMs++; } } catch (IOException expected) {} } assertEquals("Didn't get all VMs!!", MAX_BUFFERED_REPLIES, numReplyNumberVMs); // send 2 new queries that shouldn't be ACKed for (int i = 0; i < 2; i++) { query = QueryRequest.createOutOfBandQuery((i%2==0) ? "berkeley" : "susheel", InetAddress.getLocalHost().getAddress(), UDP_ACCESS.getLocalPort()); query.hop(); ULTRAPEER[0].send(query); ULTRAPEER[0].flush(); } // count NO ReplyNumberVMs try { while (true) { UDP_ACCESS.setSoTimeout(500); pack = new DatagramPacket(new byte[1000], 1000); UDP_ACCESS.receive(pack); InputStream in = new ByteArrayInputStream(pack.getData()); message = Message.read(in); assertNotInstanceof( ReplyNumberVendorMessage.class, message ); } } catch (IOException expected) {} } // make sure that the Ultrapeer discards out-of-band query requests that // have an improper address associated with it public void testIdentity() throws Exception { drainAll(); byte[] crapIP = {(byte)192,(byte)168,(byte)1,(byte)1}; QueryRequest query = QueryRequest.createOutOfBandQuery("berkeley", crapIP, 6346); LEAF[0].send(query); LEAF[0].flush(); // ultrapeers should NOT get the QR assertNull(getFirstQueryRequest(ULTRAPEER[0])); assertNull(getFirstQueryRequest(ULTRAPEER[1])); Socket socket = (Socket)PrivilegedAccessor.getValue(LEAF[0], "_socket"); // try a good query query = QueryRequest.createOutOfBandQuery("berkeley", socket.getLocalAddress().getAddress(), 6346); LEAF[0].send(query); LEAF[0].flush(); Thread.sleep(4000); // ultrapeers should get the QR assertNotNull(getFirstQueryRequest(ULTRAPEER[0]) ); assertNotNull(getFirstQueryRequest(ULTRAPEER[1]) ); // LEAF[0] should get the reply assertNotNull(getFirstQueryReply(LEAF[0])); } // a node should NOT send a reply out of band via UDP if it is not // far away (low hop) public void testLowHopOutOfBandRequest() throws Exception { drainAll(); byte[] meIP = {(byte)127,(byte)0,(byte)0,(byte)1}; QueryRequest query = QueryRequest.createOutOfBandQuery("susheel", meIP, UDP_ACCESS.getLocalPort()); ULTRAPEER[1].send(query); ULTRAPEER[1].flush(); // ULTRAPEER[1] should get a reply via TCP assertNotNull(getFirstQueryReply(ULTRAPEER[1])); } }