blob: d43f832a4b69044703c81fc9ed0c6a4a70231d37 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.net.test;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Environment;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import org.junit.Assert;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.net.X509Util;
import org.chromium.net.test.util.CertTestUtil;
import java.io.File;
/** A simple file server for java tests.
*
* An example use:
* EmbeddedTestServer s = EmbeddedTestServer.createAndStartServer(context);
*
* // serve requests...
* s.getURL("/foo/bar.txt");
*
* s.stopAndDestroyServer();
*
* Note that this runs net::test_server::EmbeddedTestServer in a service in a separate APK.
*/
public class EmbeddedTestServer {
private static final String TAG = "cr_TestServer";
private static final String EMBEDDED_TEST_SERVER_SERVICE =
"org.chromium.net.test.EMBEDDED_TEST_SERVER_SERVICE";
private static final long SERVICE_CONNECTION_WAIT_INTERVAL_MS = 5000;
private IEmbeddedTestServerImpl mImpl;
private ServiceConnection mConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mImplMonitor) {
mImpl = IEmbeddedTestServerImpl.Stub.asInterface(service);
mImplMonitor.notify();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mImplMonitor) {
mImpl = null;
mImplMonitor.notify();
}
}
};
private Context mContext;
private final Object mImplMonitor = new Object();
// Whether the server should use HTTP or HTTPS.
public enum ServerHTTPSSetting {
USE_HTTP,
USE_HTTPS,
}
/**
* Exception class raised on failure in the EmbeddedTestServer.
*/
public static final class EmbeddedTestServerFailure extends Error {
public EmbeddedTestServerFailure(String errorDesc) {
super(errorDesc);
}
public EmbeddedTestServerFailure(String errorDesc, Throwable cause) {
super(errorDesc, cause);
}
}
/**
* Connection listener class, to be notified of new connections and sockets reads.
*
* Notifications are asynchronous and delivered to the UI thread.
*/
public static class ConnectionListener {
private final IConnectionListener mListener = new IConnectionListener.Stub() {
@Override
public void acceptedSocket(final long socketId) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
ConnectionListener.this.acceptedSocket(socketId);
}
});
}
@Override
public void readFromSocket(final long socketId) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
ConnectionListener.this.readFromSocket(socketId);
}
});
}
};
/**
* A new socket connection has been opened on the server.
*
* @param socketId Socket unique identifier. Unique as long as the socket stays open.
*/
public void acceptedSocket(long socketId) {}
/**
* Data has been read from a socket.
*
* @param socketId Socket unique identifier. Unique as long as the socket stays open.
*/
public void readFromSocket(long socketId) {}
private IConnectionListener getListener() {
return mListener;
}
}
/** Bind the service that will run the native server object.
*
* @param context The context to use to bind the service. This will also be used to unbind
* the service at server destruction time.
* @param httpsSetting Whether the server should use HTTPS.
*/
public void initializeNative(Context context, ServerHTTPSSetting httpsSetting)
throws InterruptedException {
mContext = context;
Intent intent = new Intent(EMBEDDED_TEST_SERVER_SERVICE);
setIntentClassName(intent);
if (!mContext.bindService(intent, mConn, Context.BIND_AUTO_CREATE)) {
throw new EmbeddedTestServerFailure(
"Unable to bind to the EmbeddedTestServer service.");
}
synchronized (mImplMonitor) {
Log.i(TAG, "Waiting for EmbeddedTestServer service connection.");
while (mImpl == null) {
mImplMonitor.wait(SERVICE_CONNECTION_WAIT_INTERVAL_MS);
Log.i(TAG, "Still waiting for EmbeddedTestServer service connection.");
}
Log.i(TAG, "EmbeddedTestServer service connected.");
boolean initialized = false;
try {
initialized = mImpl.initializeNative(httpsSetting == ServerHTTPSSetting.USE_HTTPS);
} catch (RemoteException e) {
Log.e(TAG, "Failed to initialize native server.", e);
initialized = false;
}
if (!initialized) {
throw new EmbeddedTestServerFailure("Failed to initialize native server.");
}
if (httpsSetting == ServerHTTPSSetting.USE_HTTPS) {
try {
String rootCertPemPath = mImpl.getRootCertPemPath();
X509Util.addTestRootCertificate(CertTestUtil.pemToDer(rootCertPemPath));
} catch (Exception e) {
throw new EmbeddedTestServerFailure(
"Failed to install root certificate from native server.", e);
}
}
}
}
/** Set intent package and class name that will pass to the service.
*
* @param intent The intent to use to pass into the service.
*/
protected void setIntentClassName(Intent intent) {
intent.setClassName(
"org.chromium.net.test.support", "org.chromium.net.test.EmbeddedTestServerService");
}
/** Add the default handlers and serve files from the provided directory relative to the
* external storage directory.
*
* @param directory The directory from which files should be served relative to the external
* storage directory.
*/
public void addDefaultHandlers(File directory) {
addDefaultHandlers(directory.getPath());
}
/** Add the default handlers and serve files from the provided directory relative to the
* external storage directory.
*
* @param directoryPath The path of the directory from which files should be served relative
* to the external storage directory.
*/
public void addDefaultHandlers(String directoryPath) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
mImpl.addDefaultHandlers(directoryPath);
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure(
"Failed to add default handlers and start serving files from " + directoryPath
+ ": " + e.toString());
}
}
/** Configure the server to use a particular type of SSL certificate.
*
* @param serverCertificate The type of certificate the server should use.
*/
public void setSSLConfig(int serverCertificate) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
mImpl.setSSLConfig(serverCertificate);
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure(
"Failed to set server certificate: " + e.toString());
}
}
/** Serve files from the provided directory.
*
* @param directory The directory from which files should be served.
*/
public void serveFilesFromDirectory(File directory) {
serveFilesFromDirectory(directory.getPath());
}
/** Serve files from the provided directory.
*
* @param directoryPath The path of the directory from which files should be served.
*/
public void serveFilesFromDirectory(String directoryPath) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
mImpl.serveFilesFromDirectory(directoryPath);
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure(
"Failed to start serving files from " + directoryPath + ": " + e.toString());
}
}
/**
* Sets a connection listener. Must be called after the server has been initialized, but
* before calling {@link start()}.
*
* @param listener The listener to set.
*/
public void setConnectionListener(ConnectionListener listener) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
mImpl.setConnectionListener(listener.getListener());
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Cannot set the listener");
}
}
private void checkServiceLocked() {
if (mImpl == null) {
throw new EmbeddedTestServerFailure("Service disconnected.");
}
}
/** Starts the server with an automatically selected port.
*
* Note that this should be called after handlers are set up, including any relevant calls
* serveFilesFromDirectory.
*
* @return Whether the server was successfully initialized.
*/
public boolean start() {
return start(0);
}
/** Starts the server with the specified port.
*
* Note that this should be called after handlers are set up, including any relevant calls
* serveFilesFromDirectory.
*
* @param port The port to use for the server, 0 to auto-select an unused port.
*
* @return Whether the server was successfully initialized.
*/
public boolean start(int port) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
return mImpl.start(port);
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Failed to start server.", e);
}
}
/** Create and initialize a server that serves files from the provided directory.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param context The context in which the server will run.
* @param directory The directory from which files should be served. This must be
* Environment.getExternalStorageDirectory().
* @return The created server.
*/
public static EmbeddedTestServer createAndStartFileServer(Context context, File directory)
throws InterruptedException {
// TODO(jbudorick): Update all callers to use createAndStartServer() directly.
if (!directory.equals(Environment.getExternalStorageDirectory())) {
throw new IllegalArgumentException("Expected directory to be ExternalStorageDirectory");
}
return createAndStartServer(context);
}
/** Create and initialize a server with the default handlers.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param context The context in which the server will run.
* @return The created server.
*/
public static EmbeddedTestServer createAndStartDefaultServer(Context context)
throws InterruptedException {
// TODO(pkotwicz): Update all callers to use createAndStartServer() directly.
return createAndStartServer(context);
}
/** Create and initialize a server with the default handlers.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param context The context in which the server will run.
* @return The created server.
*/
public static EmbeddedTestServer createAndStartServer(Context context)
throws InterruptedException {
return createAndStartServerWithPort(context, 0);
}
/** Create and initialize a server with the default handlers and specified port.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param context The context in which the server will run.
* @param port The port to use for the server, 0 to auto-select an unused port.
* @return The created server.
*/
public static EmbeddedTestServer createAndStartServerWithPort(Context context, int port)
throws InterruptedException {
Assert.assertNotEquals("EmbeddedTestServer should not be created on UiThread, "
+ "the instantiation will hang forever waiting for tasks to post to UI thread",
Looper.getMainLooper(), Looper.myLooper());
EmbeddedTestServer server = new EmbeddedTestServer();
return initializeAndStartServer(server, context, port);
}
/** Create and initialize an HTTPS server with the default handlers.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param context The context in which the server will run.
* @param serverCertificate The certificate option that the server will use.
* @return The created server.
*/
public static EmbeddedTestServer createAndStartHTTPSServer(
Context context, int serverCertificate) throws InterruptedException {
return createAndStartHTTPSServerWithPort(context, serverCertificate, 0 /* port */);
}
/** Create and initialize an HTTPS server with the default handlers and specified port.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param context The context in which the server will run.
* @param serverCertificate The certificate option that the server will use.
* @param port The port to use for the server, 0 to auto-select an unused port.
* @return The created server.
*/
public static EmbeddedTestServer createAndStartHTTPSServerWithPort(
Context context, int serverCertificate, int port) throws InterruptedException {
Assert.assertNotEquals("EmbeddedTestServer should not be created on UiThread, "
+ "the instantiation will hang forever waiting for tasks"
+ " to post to UI thread",
Looper.getMainLooper(), Looper.myLooper());
EmbeddedTestServer server = new EmbeddedTestServer();
return initializeAndStartHTTPSServer(server, context, serverCertificate, port);
}
/** Initialize a server with the default handlers.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param server The server instance that will be initialized.
* @param context The context in which the server will run.
* @param port The port to use for the server, 0 to auto-select an unused port.
* @return The created server.
*/
public static <T extends EmbeddedTestServer> T initializeAndStartServer(
T server, Context context, int port) throws InterruptedException {
server.initializeNative(context, ServerHTTPSSetting.USE_HTTP);
server.addDefaultHandlers("");
if (!server.start(port)) {
throw new EmbeddedTestServerFailure("Failed to start serving using default handlers.");
}
return server;
}
/** Initialize a server with the default handlers that uses HTTPS with the given certificate
* option.
*
* This handles native object initialization, server configuration, and server initialization.
* On returning, the server is ready for use.
*
* @param server The server instance that will be initialized.
* @param context The context in which the server will run.
* @param serverCertificate The certificate option that the server will use.
* @param port The port to use for the server.
* @return The created server.
*/
public static <T extends EmbeddedTestServer> T initializeAndStartHTTPSServer(T server,
Context context, int serverCertificate, int port) throws InterruptedException {
server.initializeNative(context, ServerHTTPSSetting.USE_HTTPS);
server.addDefaultHandlers("");
server.setSSLConfig(serverCertificate);
if (!server.start(port)) {
throw new EmbeddedTestServerFailure("Failed to start serving using default handlers.");
}
return server;
}
/** Get the full URL for the given relative URL.
*
* @param relativeUrl The relative URL for which a full URL will be obtained.
* @return The URL as a String.
*/
public String getURL(String relativeUrl) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
return mImpl.getURL(relativeUrl);
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Failed to get URL for " + relativeUrl, e);
}
}
/** Get the full URL for the given relative URL. Similar to the above method but uses the given
* hostname instead of 127.0.0.1. The hostname should be resolved to 127.0.0.1.
*
* @param hostName The host name which should be used.
* @param relativeUrl The relative URL for which a full URL should be returned.
* @return The URL as a String.
*/
public String getURLWithHostName(String hostname, String relativeUrl) {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
return mImpl.getURLWithHostName(hostname, relativeUrl);
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure(
"Failed to get URL for " + hostname + " and " + relativeUrl, e);
}
}
/** Get the full URLs for the given relative URLs.
*
* @see #getURL(String)
*
* @param relativeUrls The relative URLs for which full URLs will be obtained.
* @return The URLs as a String array.
*/
public String[] getURLs(String... relativeUrls) {
String[] absoluteUrls = new String[relativeUrls.length];
for (int i = 0; i < relativeUrls.length; ++i) absoluteUrls[i] = getURL(relativeUrls[i]);
return absoluteUrls;
}
/** Shutdown the server.
*
* @return Whether the server was successfully shut down.
*/
public boolean shutdownAndWaitUntilComplete() {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
return mImpl.shutdownAndWaitUntilComplete();
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Failed to shut down.", e);
}
}
/** Destroy the native EmbeddedTestServer object. */
public void destroy() {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
mImpl.destroy();
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Failed to destroy native server.", e);
} finally {
mContext.unbindService(mConn);
}
}
/** Stop and destroy the server.
*
* This handles stopping the server and destroying the native object.
*/
public void stopAndDestroyServer() {
if (!shutdownAndWaitUntilComplete()) {
throw new EmbeddedTestServerFailure("Failed to stop server.");
}
destroy();
}
/** Get the path of the PEM file of the root cert. */
public String getRootCertPemPath() {
try {
synchronized (mImplMonitor) {
checkServiceLocked();
return mImpl.getRootCertPemPath();
}
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Failed to get root cert's path", e);
}
}
}