package org.limewire.core.impl.integration;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.limewire.core.impl.friend.FriendRemoteFileDesc;
import org.limewire.core.impl.tests.CoreGlueTestUtils;
import org.limewire.friend.api.Friend;
import org.limewire.friend.api.FriendPresence;
import org.limewire.friend.api.Network;
import org.limewire.friend.api.feature.AuthTokenFeature;
import org.limewire.friend.impl.address.FriendAddress;
import org.limewire.friend.impl.address.FriendAddressResolver;
import org.limewire.friend.impl.feature.AuthTokenImpl;
import org.limewire.gnutella.tests.LimeTestCase;
import org.limewire.io.Address;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.GUID;
import org.limewire.lifecycle.ServiceRegistry;
import org.limewire.listener.EventBroadcaster;
import org.limewire.listener.EventListener;
import org.limewire.net.ConnectivityChangeEvent;
import org.limewire.net.SocketsManager;
import org.limewire.net.address.AddressFactory;
import org.limewire.net.address.AddressResolutionObserver;
import org.limewire.util.StringUtils;
import org.limewire.util.TestUtils;
import org.mortbay.http.HttpContext;
import org.mortbay.http.HttpFields;
import org.mortbay.http.HttpMessage;
import org.mortbay.http.HttpResponse;
import org.mortbay.http.HttpServer;
import org.mortbay.http.SocketListener;
import org.mortbay.http.handler.NotFoundHandler;
import org.mortbay.http.handler.ResourceHandler;
import org.mortbay.util.B64Code;
import org.mortbay.util.Resource;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.limegroup.gnutella.DownloadServices;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.downloader.DownloadStateEvent;
import com.limegroup.gnutella.downloader.ManagedDownloader;
/**
* Collection of friend download integration tests. This tests the friend download
* code and will attempt to download from an http server run on localhost.
* <p/>
* The test code will mock out or simulate the server
* behavior, and we are testing LimeWire's friend download behavior.
*/
public class FriendDownloadTest extends LimeTestCase {
private static final String FRIEND_ID = "limebuddytest@gmail.com";
private static final String AUTH_TOKEN = "authToken";
private static final String DOWNLOADER_LOGIN_ID = "limedownloader@gmail.com";
private static final String RESOURCE = "Home1234567";
private static final String PRESENCE_ID = FRIEND_ID + "/" + RESOURCE;
private static final String HOST = "localhost";
private static final String FILE_TO_DOWNLOAD_URN = "urn:sha1:GLIQY64M7FSXBSQEZY37FIM5QQSA2OUJ";
private static final int SIZE_OF_DOWNLOAD = 26;
private static final String FRIEND_DOWNLOAD_PREFIX = "/friend/download/";
private static final int PORT = 8000;
private static final int DOWNLOAD_WAIT_MILLIS = 10000;
private static final String FILE_NAME = "alphabet test file#2.txt";
private HttpServer server;
private Injector injector;
private ServiceRegistry registry;
private Mockery context;
private RemoteFileDesc remoteFileDesc;
private DownloadServices dlServices;
// keeps track of whether we are designating the sharing friend (the one who is sharing files)
// as "signed in" or not, since we are not actually logging in to a server
private AtomicBoolean isSharingFriendLoggedIn = new AtomicBoolean(true);
// used to signify connection state changes because this is
// how LW (specifically ManagedDownloader) knows when to retry downloads
private EventBroadcaster<ConnectivityChangeEvent> connectivityBroadcaster;
public FriendDownloadTest(String name) {
super(name);
}
@Override
protected void setUp() throws Exception {
context = new Mockery();
server = new HttpServer();
injector = CoreGlueTestUtils.createInjectorAndStart();
registry = injector.getInstance(ServiceRegistry.class);
context = new Mockery();
connectivityBroadcaster = injector.getInstance(
Key.get(new TypeLiteral<EventBroadcaster<ConnectivityChangeEvent>>() {
}));
dlServices = injector.getInstance(DownloadServices.class);
remoteFileDesc = getRemoteFileDesc();
registry.initialize();
registry.start();
}
private RemoteFileDesc getRemoteFileDesc() throws IOException {
AddressFactory factory = injector.getInstance(AddressFactory.class);
LocalhostFriendAddressResolver resolver = new LocalhostFriendAddressResolver();
resolver.initialize();
FriendAddress xmppAddress = new FriendAddress(PRESENCE_ID);
URN sha1Urn = URN.createSHA1Urn(FILE_TO_DOWNLOAD_URN);
Set<URN> sha1UrnSet = new HashSet<URN>();
sha1UrnSet.add(sha1Urn);
return new FriendRemoteFileDesc(xmppAddress, 1, FILE_NAME, SIZE_OF_DOWNLOAD,
GUID.makeGuid(), 1, 1, null, sha1UrnSet, "vendor", -1, false, factory, resolver);
}
@Override
protected void tearDown() throws Exception {
this.server.stop();
this.registry.stop();
}
/**
* A friend download from start to finish where the friend source is online the whole time.
*/
public void testStartToFinishFriendIsAlwaysOnline() throws Exception {
setupAndStartServer(new TestResourceHandler());
setSharingFriendSignedIn(true);
Downloader dl = dlServices.download(new RemoteFileDesc[]{remoteFileDesc}, true, new GUID());
assertNotNull(dl);
assertTrue(waitForDownloadState(dl, DOWNLOAD_WAIT_MILLIS, Downloader.DownloadState.COMPLETE));
assertEquals(dl.getState(), Downloader.DownloadState.COMPLETE);
}
/**
* A friend download should never complete if the friend is
* offline for the entire duration of the test
*/
public void testStartToFinishFriendIsAlwaysOffline() throws Exception {
// Set sharing friend to be "not signed in". Attempt download.
setupAndStartServer(new TestResourceHandler());
setSharingFriendSignedIn(false);
Downloader dl = dlServices.download(new RemoteFileDesc[]{remoteFileDesc}, true, new GUID());
assertFalse(waitForDownloadState(dl, DOWNLOAD_WAIT_MILLIS, Downloader.DownloadState.COMPLETE));
assertFalse(dl.isCompleted());
}
/**
* A friend download that starts while the friend is not online, then the friend becomes available.
*/
public void testDownloadStartsWhileFriendNotOnlineComesOnlineLater() throws Exception {
// Set sharing friend to be "not signed in". Attempt download.
setupAndStartServer(new TestResourceHandler());
setSharingFriendSignedIn(false);
Downloader dl = dlServices.download(new RemoteFileDesc[]{remoteFileDesc}, true, new GUID());
// Sleep some time for the download to start.
// Then change the sharing friend to "signed in"
Thread.sleep(3000);
// Download should most definitely not be complete!
assertFalse(dl.isCompleted());
setSharingFriendSignedIn(true);
// Wait for download completion.
assertTrue(waitForDownloadState(dl, DOWNLOAD_WAIT_MILLIS, Downloader.DownloadState.COMPLETE));
assertEquals(dl.getState(), Downloader.DownloadState.COMPLETE);
}
/**
* A friend download where the friend goes offline (network outage) and becomes available
* again, testing if the download is resumed correctly.
*/
public void testFriendOutageDownloadResume() throws Exception {
setSharingFriendSignedIn(true);
final AtomicBoolean shouldTruncate = new AtomicBoolean(true);
setupAndStartServer(new TestResourceHandler() {
@Override
protected void sendData(org.mortbay.http.HttpRequest httpRequest,
org.mortbay.http.HttpResponse httpResponse) throws IOException {
// this flag is used to trigger the http server into uploading half the file's bytes
if (shouldTruncate.get()) {
httpHeader.put("Range", "bytes=0-" + SIZE_OF_DOWNLOAD / 2);
}
super.sendData(httpRequest, httpResponse);
}
});
// start download
Downloader dl = dlServices.download(new RemoteFileDesc[]{remoteFileDesc}, true, new GUID());
// wait and verify the downloading client has effectively given up
assertTrue(waitForDownloadState(dl, DOWNLOAD_WAIT_MILLIS, Downloader.DownloadState.DOWNLOADING));
server.stop();
// sleep for a little, and make sure the downloader is not
// in a terminal state
Thread.sleep(5000);
assertFalse(dl.isCompleted());
assertTrue(dl.isResumable());
setSharingFriendSignedIn(false);
// tell the http server request handler to upload the entire range of the file
shouldTruncate.set(false);
// restart the http server
assertFalse(server.isStarted());
server.start();
// set sharing friend as being online. this should kick off a downloading retry
setSharingFriendSignedIn(true);
// Wait for download completion.
assertTrue(waitForDownloadState(dl, DOWNLOAD_WAIT_MILLIS, Downloader.DownloadState.COMPLETE));
assertEquals(dl.getState(), Downloader.DownloadState.COMPLETE);
}
/**
* A friend download where the friend cannot authenticate.
*/
public void testFriendCannotAuthenticate() throws Exception {
setSharingFriendSignedIn(true);
setupAndStartServer(new TestResourceHandler() {
@Override
protected void sendData(org.mortbay.http.HttpRequest httpRequest,
org.mortbay.http.HttpResponse httpResponse) throws IOException {
httpResponse.sendError(HttpResponse.__401_Unauthorized);
}
});
Downloader dl = dlServices.download(new RemoteFileDesc[]{remoteFileDesc}, true, new GUID());
// Wait for download completion. Download should fail.
assertFalse(waitForDownloadState(dl, DOWNLOAD_WAIT_MILLIS, Downloader.DownloadState.COMPLETE));
assertFalse(dl.isCompleted());
}
/**
* Starts the jetty http server given a {@link TestResourceHandler}
*
* @param requestHandler TestResourceHandler to handle http requests
* @throws Exception if server fails to start
*/
private void setupAndStartServer(TestResourceHandler requestHandler) throws Exception {
SocketListener listener = new SocketListener();
listener.setPort(PORT);
listener.setMinThreads(1);
server.addListener(listener);
HttpContext context = server.addContext(FRIEND_DOWNLOAD_PREFIX);
String fileDir = TestUtils.getResourceInPackage(
FILE_NAME, getClass()).getParentFile().getAbsolutePath();
context.setResourceBase(fileDir);
requestHandler.setAcceptRanges(true);
requestHandler.setDirAllowed(true);
context.addHandler(requestHandler);
context.addHandler(new NotFoundHandler());
server.start();
}
/**
* Wait for the downloader to be at a certain download state.
* Returns true if download state was arrived at before timeout occurs.
*
* @param dl Downloader to wait for
* @param milliSecondsToWait time in milliseconds to wait
* @param state download state waiting for
* @return true if the download state was reached before the time elapsed.
* false if time elapsed first.
* @throws InterruptedException if thread is interrupted
*/
private boolean waitForDownloadState(Downloader dl, long milliSecondsToWait,
final Downloader.DownloadState state) throws InterruptedException {
final ManagedDownloader mdl = (ManagedDownloader) dl;
final CountDownLatch latch = new CountDownLatch(1);
EventListener<DownloadStateEvent> dlListener = new EventListener<DownloadStateEvent>() {
@Override
public void handleEvent(DownloadStateEvent event) {
if (event.getType() == state) {
latch.countDown();
}
}
};
mdl.addListener(dlListener);
return latch.await(milliSecondsToWait, TimeUnit.MILLISECONDS);
}
private void setSharingFriendSignedIn(boolean signedIn) {
if (isSharingFriendLoggedIn.getAndSet(signedIn) != signedIn) {
connectivityBroadcaster.broadcast(new ConnectivityChangeEvent());
}
}
/**
* An address resolver that always resolves to localhost as long as the friend
* associated with the xmpp address is deemed to be signed in. Otherwise, it
* does not resolve.
*/
private class LocalhostFriendAddressResolver extends FriendAddressResolver {
private FriendPresence mockFriendPresence;
private final AuthTokenFeature authTokenFeature = new AuthTokenFeature(new AuthTokenImpl(StringUtils.toAsciiBytes(AUTH_TOKEN)));
public LocalhostFriendAddressResolver() {
super(null, null, null, null);
initFriendPresence();
}
public void initialize() {
SocketsManager mgr = injector.getInstance(SocketsManager.class);
mgr.registerResolver(this);
}
@Override
public boolean canResolve(Address address) {
return (address instanceof FriendAddress) && isSharingFriendLoggedIn.get();
}
@Override
public <T extends AddressResolutionObserver> T resolve(Address address, T observer) {
Address resolvedAddress = null;
try {
resolvedAddress = new ConnectableImpl(HOST, PORT, false);
} catch (UnknownHostException e) {
fail(HOST + " is an unknown host", e);
}
observer.resolved(resolvedAddress);
return observer;
}
@Override
public FriendPresence getPresence(FriendAddress address) {
return mockFriendPresence;
}
private void initFriendPresence() {
mockFriendPresence = context.mock(FriendPresence.class);
final Friend friend = context.mock(Friend.class);
final Network network = context.mock(Network.class);
final String canonLocalId = DOWNLOADER_LOGIN_ID;
context.checking(new Expectations() {
{
allowing(mockFriendPresence).getFeature(AuthTokenFeature.ID);
will(returnValue(authTokenFeature));
allowing(mockFriendPresence).getFriend();
will(returnValue(friend));
allowing(friend).getNetwork();
will(returnValue(network));
allowing(network).getCanonicalizedLocalID();
will(returnValue(canonLocalId));
}
});
}
}
/**
* HTTP Request handler code. May override {@link #sendData}
* method to specify behavior than just uploading the expected file.
*/
private class TestResourceHandler extends ResourceHandler {
// used to get at and set HTTP header info
protected HttpFields httpHeader;
@Override
public void handleGet(org.mortbay.http.HttpRequest httpRequest,
org.mortbay.http.HttpResponse httpResponse,
String s, java.lang.String s1,
org.mortbay.util.Resource resource) throws java.io.IOException {
super.handleGet(httpRequest, httpResponse, s, s1, resource);
parseAndValidate(httpRequest);
sendData(httpRequest, httpResponse);
}
private void parseAndValidate(org.mortbay.http.HttpRequest httpRequest) throws UnsupportedEncodingException {
// validate request. Failure means a bad request, and there is a bug in Downloading code
String request = httpRequest.getURI().toString();
String[] splitQuery = StringUtils.split(request, '/');
String friendIdParsed = org.mortbay.util.UrlEncoded.decodeString(splitQuery[2], 0, splitQuery[2].length(), "UTF-8");
String sha1Urn = splitQuery[4].substring(4);
if (!(request.startsWith(FRIEND_DOWNLOAD_PREFIX))) {
fail("Friend download HTTP Request must begin with " + FRIEND_DOWNLOAD_PREFIX);
} else if (!sha1Urn.equals(FILE_TO_DOWNLOAD_URN)) {
fail("URN mismatch; Expected URN " + FILE_TO_DOWNLOAD_URN);
}
// parse and base 64 decode the auth token
httpRequest.setState(HttpMessage.__MSG_EDITABLE);
httpHeader = httpRequest.getHeader();
String[] authHeaderSplit = httpHeader.get("Authorization").split(" ");
String authToken = B64Code.decode(authHeaderSplit[1], "UTF-8");
// check auth token to make sure the user is authorized!
if (!friendIdParsed.equals(DOWNLOADER_LOGIN_ID) || !authToken.endsWith(":" + AUTH_TOKEN)) {
fail("Friend " + friendIdParsed + " has wrong auth token " + authToken);
}
}
protected void sendData(org.mortbay.http.HttpRequest httpRequest,
org.mortbay.http.HttpResponse httpResponse) throws IOException {
// send back file
httpResponse.setStatus(org.mortbay.http.HttpResponse.__200_OK);
Resource res = getResource(FILE_NAME);
sendData(httpRequest, httpResponse, "./", res, true);
httpResponse.commit();
}
}
}