blob: 0b3d9979e09b3afd2fcbb81fefe8744f637ac4cf [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// 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.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import androidx.annotation.GuardedBy;
import org.junit.Assert;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner.ClassHook;
import org.chromium.base.test.util.UrlUtils;
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:
* <pre>
* EmbeddedTestServer s = EmbeddedTestServer.createAndStartServer(context);
*
* // serve requests...
* s.getURL("/foo/bar.txt");
*
* // Generally safe to omit as ResettersForTesting will call it.
* s.stopAndDestroyServer();
* </pre>
*
* Note that this runs net::test_server::EmbeddedTestServer in a service in a separate APK.
*/
public class EmbeddedTestServer {
private static final String TAG = "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 static boolean sTestRootInitDone;
@GuardedBy("mImplMonitor")
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();
boolean mDisableResetterForTesting;
// 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(
() -> {
ConnectionListener.this.acceptedSocket(socketId);
});
}
@Override
public void readFromSocket(final long socketId) {
ThreadUtils.runOnUiThread(
() -> {
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) {
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) {
try {
mImplMonitor.wait(SERVICE_CONNECTION_WAIT_INTERVAL_MS);
} catch (InterruptedException e) {
// Ignore the InterruptedException. Rely on the outer while loop to re-run.
}
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 (!mDisableResetterForTesting) {
ResettersForTesting.register(this::stopAndDestroyServer);
}
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(@ServerCertificate 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");
}
}
@GuardedBy("mImplMonitor")
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 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) {
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) {
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, @ServerCertificate int serverCertificate) {
return createAndStartHTTPSServerWithPort(context, serverCertificate, /* port= */ 0);
}
/** 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, @ServerCertificate int serverCertificate, int port) {
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) {
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, @ServerCertificate int serverCertificate, int port) {
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;
}
/**
* Stop and destroy the server.
*
* This handles stopping the server and destroying the native object.
*/
public void stopAndDestroyServer() {
synchronized (mImplMonitor) {
// ResettersForTesting call can cause this to be called multiple times.
if (mImpl == null) {
return;
}
try {
if (!mImpl.shutdownAndWaitUntilComplete()) {
throw new EmbeddedTestServerFailure("Failed to stop server.");
}
mImpl.destroy();
mImpl = null;
} catch (RemoteException e) {
throw new EmbeddedTestServerFailure("Failed to shut down.", e);
} finally {
mContext.unbindService(mConn);
}
}
}
/** 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);
}
}
public static ClassHook getPreClassHook() {
return (targetContext, testClass) -> EmbeddedTestServer.setUpClass(testClass);
}
public static void setUpClass(Class<?> clazz) {
if (sTestRootInitDone) {
return;
}
// Always try to add the testing HTTPS root to the cert verifier. We do this here because we
// need this to happen before the native code loads the user-added roots, and this is the
// safest place to put it.
try {
// Use the same PEM file as net/test/embedded_test_server/embedded_test_server.cc.
String rootCertPemPath =
UrlUtils.getIsolatedTestFilePath("net/data/ssl/certificates/root_ca_cert.pem");
byte[] rootCertBytesDer = CertTestUtil.pemToDer(rootCertPemPath);
X509Util.setTestRootCertificateForBuiltin(rootCertBytesDer);
sTestRootInitDone = true;
} catch (Exception e) {
throw new EmbeddedTestServer.EmbeddedTestServerFailure(
"Failed to install root certificate.", e);
}
}
}