package com.limegroup.gnutella;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import junit.framework.Test;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.core.api.connection.FirewallStatus;
import org.limewire.core.api.connection.FirewallStatusEvent;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.io.IOUtils;
import org.limewire.io.NetworkUtils;
import org.limewire.listener.EventListener;
import org.limewire.listener.ListenerSupport;
import org.limewire.net.ConnectionAcceptor;
import org.limewire.net.ConnectionDispatcher;
import org.limewire.net.SocketsManager;
import org.limewire.net.SocketsManager.ConnectType;
import org.limewire.nio.ssl.SSLUtils;
import org.limewire.nio.ssl.TLSNIOSocket;
import org.limewire.util.OSUtils;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.limegroup.gnutella.util.LimeTestCase;
public class AcceptorTest extends LimeTestCase {
private static final Log LOG = LogFactory.getLog(AcceptorTest.class);
private Injector injector;
private AcceptorImpl acceptor;
private ConnectionDispatcher connectionDispatcher;
private StubCM connectionManager;
private StubAC activityCallback;
private SocketsManager socketsManager;
public AcceptorTest(String name) {
super(name);
}
public static Test suite() {
return buildTestSuite(AcceptorTest.class);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
@Override
public void setUp() throws Exception {
connectionManager = new StubCM();
activityCallback = new StubAC();
injector = LimeTestUtils.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(ConnectionManager.class).toInstance(connectionManager);
bind(ActivityCallback.class).toInstance(activityCallback);
}
});
injector.getInstance(Key.get(new TypeLiteral<ListenerSupport<FirewallStatusEvent>>(){})).addListener(activityCallback);
connectionDispatcher = injector.getInstance(Key.get(ConnectionDispatcher.class, Names.named("global")));
socketsManager = injector.getInstance(SocketsManager.class);
acceptor = (AcceptorImpl)injector.getInstance(Acceptor.class);
acceptor.setIncomingExpireTime(2000);
acceptor.setTimeBetweenValidates(2000);
acceptor.setWaitTimeAfterRequests(2000);
acceptor.start();
//shut off the various services,
//if an exception is thrown, something bad happened.
acceptor.setListeningPort(0);
}
@Override
public void tearDown() throws Exception {
//shut off the various services,
//if an exception is thrown, something bad happened.
acceptor.setListeningPort(0);
ConnectionSettings.LOCAL_IS_PRIVATE.revertToDefault();
}
public void testValidateIncomingTimer() throws Exception {
ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);
int port = bindAcceptor();
assertEquals(0, activityCallback.getChanges());
assertFalse(activityCallback.getLastStatus());
connectionManager.setShouldSendRequests(true);
assertTrue(connectionManager.awaitSend());
// make sure we don't send it again which could screw the test!
connectionManager.setShouldSendRequests(false);
Thread.sleep(500); // Make sure the resetter gets scheduled.
// Turn incoming on, make sure it triggers a change...
activityCallback.resetLatch();
acceptor.setIncoming(true);
activityCallback.waitForSingleChange(true);
assertEquals(1, activityCallback.getChanges());
assertTrue(activityCallback.getLastStatus());
// Make sure we revert back to false, since no incoming came...
connectionManager.setShouldSendRequests(true);
assertTrue(connectionManager.awaitSend());
connectionManager.setShouldSendRequests(false);
assertFalse(activityCallback.waitForNextChange());
assertEquals(2, activityCallback.getChanges());
// Turn incoming on, make sure we get the status...
activityCallback.resetLatch();
acceptor.setIncoming(true);
activityCallback.waitForSingleChange(true);
assertEquals(3, activityCallback.getChanges());
assertTrue(activityCallback.getLastStatus());
// Send another request, but this time we're gonna make sure
// incoming stays on!
connectionManager.setShouldSendRequests(true);
assertTrue(connectionManager.awaitSend());
connectionManager.setShouldSendRequests(false);
// Now send the connectback..
Socket socket = socketsManager.connect(new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(), port), 1000);
socket.getOutputStream().write("CONNECT BACK\r\n".getBytes());
socket.getOutputStream().flush();
IOUtils.close(socket);
// Sleep a bit, make sure incoming is still on!
Thread.sleep(10000);
// Note that the last status of CM.shouldSendRequests is false,
// so that future checks won't schedule a resetter.
// We're only concerned with the one that we waited for
// and send a connectback from.
// This just validates that if we receive a connectback,
// the resetter is cancelled.
assertTrue(acceptor.acceptedIncoming());
assertEquals(3, activityCallback.getChanges());
assertTrue(activityCallback.getLastStatus());
}
/**
* This test checks to ensure that Acceptor.setListeningPort
* cannot use a port if the UDP part is already bound.
*/
public void testCannotUseBoundUDPPort() {
int portToTry = 2000;
DatagramSocket udp = null;
while (true) {
// get a free port for UDP traffic.
try {
udp = new DatagramSocket(portToTry);
break;
}
catch (IOException e) {
portToTry++;
continue;
}
}
try {
acceptor.setListeningPort(portToTry);
assertTrue("had no trouble binding UDP port!", false);
} catch (IOException expected) {
IOUtils.close(udp);
}
}
/**
* This test checks to ensure that Acceptor.setListeningPort
* cannot use port if the TCP part is already bound.
*/
public void testCannotUseBoundTCPPort() {
int portToTry = 2000;
ServerSocket tcp = null;
while (true) {
// get a free port for UDP traffic.
try {
tcp = new ServerSocket(portToTry);
break;
} catch (IOException e) {
portToTry++;
continue;
}
}
try {
acceptor.setListeningPort(portToTry);
if (OSUtils.isWindows())
fail("jvm oddity - disable socketResueAddress");
else
fail("had no trouble binding TCP port!");
}
catch (IOException expected) {
IOUtils.close(tcp);
}
}
/**
* This test checks to make sure that Acceptor.setListeningPort
* correctly binds the UDP & TCP port.
*/
public void testAcceptorBindsUDPandTCP() {
int portToTry = 2000;
while (true) {
try {
acceptor.setListeningPort(portToTry);
break;
} catch (IOException occupied) {
portToTry++;
continue;
}
}
try {
DatagramSocket udp = new DatagramSocket(portToTry);
udp.close();
fail("had no trouble binding UDP to occupied port!");
} catch (IOException good) {
}
try {
ServerSocket tcp = new ServerSocket(portToTry);
tcp.close();
fail("had no trouble binding TCP to occupied port!");
} catch (IOException good) {
}
}
public void testAcceptedIncoming() throws Exception {
ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);
int port = bindAcceptor();
assertFalse(acceptor.acceptedIncoming());
// open up incoming to the test node
{
Socket sock = null;
OutputStream os = null;
try {
sock=socketsManager.connect(new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(),
port), 12);
os = sock.getOutputStream();
os.write("\n\n".getBytes());
os.flush();
} catch (IOException ignored) {
} catch (SecurityException ignored) {
} finally {
IOUtils.close(sock);
IOUtils.close(os);
}
}
Thread.sleep(250);
// CONNECT-BACK is hardcoded on
assertFalse(acceptor.acceptedIncoming());
}
public void testAcceptedConnectBack() throws Exception {
ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);
int port = bindAcceptor();
assertFalse(acceptor.acceptedIncoming());
// open up incoming to the test node
{
Socket sock = null;
OutputStream os = null;
try {
sock=socketsManager.connect(new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(),
port), 12);
os = sock.getOutputStream();
os.write("CONNECT ".getBytes());
os.flush();
} catch (IOException ignored) {
} catch (SecurityException ignored) {
} finally {
IOUtils.close(sock);
IOUtils.close(os);
}
}
Thread.sleep(250);
// test on acceptor since network manager is stubbed
assertTrue(acceptor.acceptedIncoming());
}
public void testTLSAcceptedIncoming() throws Exception {
ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);
int port = bindAcceptor();
assertFalse(acceptor.acceptedIncoming());
// open up incoming to the test node
{
Socket sock = null;
OutputStream os = null;
try {
sock=socketsManager.connect(new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(),
port), 12, ConnectType.TLS);
os = sock.getOutputStream();
os.write("\n\n".getBytes());
os.flush();
} catch (IOException ignored) {
} catch (SecurityException ignored) {
} finally {
IOUtils.close(sock);
IOUtils.close(os);
}
}
Thread.sleep(250);
// CONNECT-BACK is hardcoded on
assertFalse(acceptor.acceptedIncoming());
}
public void testTLSAcceptedConnectBack() throws Exception {
ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);
int port = bindAcceptor();
assertFalse(acceptor.acceptedIncoming());
// open up incoming to the test node
{
Socket sock = null;
OutputStream os = null;
try {
sock=socketsManager.connect(new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(),
port), 12, ConnectType.TLS);
os = sock.getOutputStream();
os.write("CONNECT ".getBytes());
os.flush();
} catch (IOException ignored) {
} catch (SecurityException ignored) {
} finally {
IOUtils.close(sock);
IOUtils.close(os);
}
}
Thread.sleep(250);
// test on acceptor since network manager is stubbed
assertTrue(acceptor.acceptedIncoming());
}
public void testIncomingTLSHomemadeTLS() throws Exception {
int port = bindAcceptor();
MyConnectionAcceptor acceptor = new MyConnectionAcceptor("BLOCKING");
connectionDispatcher.addConnectionAcceptor(acceptor, true, "BLOCKING");
Socket tls = new TLSNIOSocket("localhost", port);
tls.getOutputStream().write("BLOCKING MORE DATA".getBytes());
tls.getOutputStream().flush();
assertTrue(acceptor.waitForAccept());
tls.close();
}
public void testIncomingTLSBuiltIn() throws Exception {
int port = bindAcceptor();
MyConnectionAcceptor acceptor = new MyConnectionAcceptor("BLOCKING");
connectionDispatcher.addConnectionAcceptor(acceptor, true, "BLOCKING");
SSLContext context = SSLUtils.getTLSContext();
SSLSocket tls = (SSLSocket)context.getSocketFactory().createSocket();
tls.setUseClientMode(true);
tls.setEnabledCipherSuites(new String[] { "TLS_DH_anon_WITH_AES_128_CBC_SHA" } );
tls.connect(new InetSocketAddress("localhost", port));
tls.getOutputStream().write("BLOCKING MORE DATA".getBytes());
tls.getOutputStream().flush();
assertTrue(acceptor.waitForAccept());
tls.close();
}
public void testAcceptorPortForcing() throws Exception {
int localPort = acceptor.getPort(false);
ConnectionSettings.FORCED_PORT.setValue(1000);
ConnectionSettings.FORCE_IP_ADDRESS.setValue(true);
ConnectionSettings.FORCED_IP_ADDRESS_STRING.setValue(InetAddress.getLocalHost().getHostAddress());
assertEquals(1000, acceptor.getPort(true));
assertNotEquals(1000,localPort);
}
/**
* Ensures the external address is returned as forced address if the client
* accepts incoming connections. This can happen if port forwarding is configured
* in the firewall but not in the client.
*
* This is necessary to ensure that the client advertises its external address
* correctly to peers.
*/
public void testGetAddressReturnsForcedExternalAddressIfAcceptedIncoming() throws Exception {
acceptor.setAcceptedIncoming(true);
assertTrue(acceptor.acceptedIncoming());
acceptor.setExternalAddress(InetAddress.getByName("129.0.0.2"));
assertTrue(NetworkUtils.isValidAddress(acceptor.getExternalAddress()));
assertFalse(ConnectionSettings.FORCE_IP_ADDRESS.getValue());
assertNotEquals(new byte[] { (byte)129, 0, 0, 2}, acceptor.getAddress(false));
assertEquals(new byte[] { (byte)129, 0, 0, 2}, acceptor.getAddress(true));
}
private int bindAcceptor() throws Exception {
for (int p = 2000; p < Integer.MAX_VALUE; p++) {
try {
acceptor.setListeningPort(p);
return p;
} catch (IOException ignored) {
}
}
throw new IOException("unable to bind acceptor");
}
private static class MyConnectionAcceptor implements ConnectionAcceptor {
private final String word;
private CountDownLatch latch = new CountDownLatch(1);
MyConnectionAcceptor(String word) {
this.word = word;
}
public void acceptConnection(String word, Socket s) {
LOG.debug("Got connection for word: " + word + ", socket: " + s);
assertEquals(this.word, word);
if ("BLOCKING".equals(word)) {
try {
LOG.debug("Getting IS");
InputStream in = s.getInputStream();
byte[] b = new byte[1000];
LOG.debug("Reading");
int read = in.read(b);
LOG.debug("read");
assertEquals("MORE DATA", new String(b, 0, read));
latch.countDown();
s.close();
} catch (IOException iox) {
throw new RuntimeException(iox);
}
}
}
boolean waitForAccept() throws Exception {
return latch.await(5, TimeUnit.SECONDS);
}
public boolean isBlocking() {
return true;
}
}
private static class StubCM extends ConnectionManagerAdapter {
private volatile CountDownLatch latch;
private volatile boolean shouldSendRequests;
void setShouldSendRequests(boolean send) {
this.latch = new CountDownLatch(1);
this.shouldSendRequests = send;
}
boolean awaitSend() throws InterruptedException {
return latch.await(8000, TimeUnit.MILLISECONDS);
}
@Override
public boolean sendTCPConnectBackRequests() {
if(shouldSendRequests) {
latch.countDown();
return true;
}
return false;
}
}
private static class StubAC extends ActivityCallbackAdapter implements EventListener<FirewallStatusEvent> {
private volatile int changes = 0;
private volatile boolean lastStatus = false;
private volatile CountDownLatch latch;
private volatile CountDownLatch singleChangeLatch;
void resetLatch() {
singleChangeLatch = new CountDownLatch(1);
}
@Override
public void handleEvent(FirewallStatusEvent event) {
changes++;
lastStatus = event.getSource() == FirewallStatus.NOT_FIREWALLED;
if(latch != null)
latch.countDown();
if(singleChangeLatch != null) {
singleChangeLatch.countDown();
}
}
int waitForSingleChange(boolean expect) throws Exception {
assertEquals(expect, singleChangeLatch.await(500, TimeUnit.MILLISECONDS));
return changes;
}
int getChanges() {
return changes;
}
boolean getLastStatus() {
return lastStatus;
}
boolean waitForNextChange() throws InterruptedException {
latch = new CountDownLatch(1);
if(!latch.await(5000, TimeUnit.MILLISECONDS))
fail("Didn't get countdown");
return lastStatus;
}
}
}