| Index: android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java
|
| diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java b/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..8508a51f1a83c3667e4b4312ea29d91e52105c7d
|
| --- /dev/null
|
| +++ b/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java
|
| @@ -0,0 +1,431 @@
|
| +// Copyright (c) 2012 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.android_webview.test;
|
| +
|
| +import android.util.Base64;
|
| +import android.util.Log;
|
| +import android.util.Pair;
|
| +
|
| +import org.apache.http.HttpException;
|
| +import org.apache.http.HttpRequest;
|
| +import org.apache.http.HttpResponse;
|
| +import org.apache.http.HttpStatus;
|
| +import org.apache.http.HttpVersion;
|
| +import org.apache.http.RequestLine;
|
| +import org.apache.http.StatusLine;
|
| +import org.apache.http.entity.StringEntity;
|
| +import org.apache.http.impl.DefaultHttpServerConnection;
|
| +import org.apache.http.impl.cookie.DateUtils;
|
| +import org.apache.http.message.BasicHttpResponse;
|
| +import org.apache.http.params.BasicHttpParams;
|
| +import org.apache.http.params.CoreProtocolPNames;
|
| +import org.apache.http.params.HttpParams;
|
| +
|
| +import java.io.ByteArrayInputStream;
|
| +import java.io.IOException;
|
| +import java.io.InputStream;
|
| +import java.io.UnsupportedEncodingException;
|
| +import java.net.MalformedURLException;
|
| +import java.net.ServerSocket;
|
| +import java.net.Socket;
|
| +import java.net.URI;
|
| +import java.net.URL;
|
| +import java.net.URLConnection;
|
| +import java.security.KeyManagementException;
|
| +import java.security.KeyStore;
|
| +import java.security.NoSuchAlgorithmException;
|
| +import java.security.cert.X509Certificate;
|
| +import java.util.ArrayList;
|
| +import java.util.Date;
|
| +import java.util.HashMap;
|
| +import java.util.Hashtable;
|
| +import java.util.List;
|
| +import java.util.Map;
|
| +
|
| +import javax.net.ssl.HostnameVerifier;
|
| +import javax.net.ssl.HttpsURLConnection;
|
| +import javax.net.ssl.KeyManager;
|
| +import javax.net.ssl.KeyManagerFactory;
|
| +import javax.net.ssl.SSLContext;
|
| +import javax.net.ssl.SSLSession;
|
| +import javax.net.ssl.X509TrustManager;
|
| +
|
| +/**
|
| + * Simple http test server for testing.
|
| + *
|
| + * Based heavily on the CTSWebServer in Android.
|
| + */
|
| +public class TestWebServer {
|
| + private static final String TAG = "TestWebServer";
|
| + private static final int SERVER_PORT = 4444;
|
| + private static final int SSL_SERVER_PORT = 4445;
|
| +
|
| + public static final String SHUTDOWN_PREFIX = "/shutdown";
|
| +
|
| + private static TestWebServer sInstance;
|
| + private static Hashtable<Integer, String> sReasons;
|
| +
|
| + private ServerThread mServerThread;
|
| + private String mServerUri;
|
| + private boolean mSsl;
|
| +
|
| + private static class Response {
|
| + final String mResponseStr;
|
| + final List<Pair<String, String>> mResponseHeaders;
|
| +
|
| + Response(String responseStr, List<Pair<String, String>> responseHeaders) {
|
| + mResponseStr = responseStr;
|
| + mResponseHeaders = responseHeaders == null ?
|
| + new ArrayList<Pair<String, String>>() : responseHeaders;
|
| + }
|
| + }
|
| +
|
| + private Map<String, Response> mResponseMap = new HashMap<String, Response>();
|
| +
|
| + /**
|
| + * Create and start a local HTTP server instance.
|
| + * @param ssl True if the server should be using secure sockets.
|
| + * @throws Exception
|
| + */
|
| + public TestWebServer(boolean ssl) throws Exception {
|
| + if (sInstance != null) {
|
| + // attempt to start a new instance while one is still running
|
| + // shut down the old instance first
|
| + sInstance.shutdown();
|
| + }
|
| + sInstance = this;
|
| + mSsl = ssl;
|
| + if (mSsl) {
|
| + mServerUri = "https://localhost:" + SSL_SERVER_PORT;
|
| + } else {
|
| + mServerUri = "http://localhost:" + SERVER_PORT;
|
| + }
|
| + mServerThread = new ServerThread(this, mSsl);
|
| + mServerThread.start();
|
| + }
|
| +
|
| + /**
|
| + * Terminate the http server.
|
| + */
|
| + public void shutdown() {
|
| + try {
|
| + // Avoid a deadlock between two threads where one is trying to call
|
| + // close() and the other one is calling accept() by sending a GET
|
| + // request for shutdown and having the server's one thread
|
| + // sequentially call accept() and close().
|
| + URL url = new URL(mServerUri + SHUTDOWN_PREFIX);
|
| + URLConnection connection = openConnection(url);
|
| + connection.connect();
|
| +
|
| + // Read the input from the stream to send the request.
|
| + InputStream is = connection.getInputStream();
|
| + is.close();
|
| +
|
| + // Block until the server thread is done shutting down.
|
| + mServerThread.join();
|
| +
|
| + } catch (MalformedURLException e) {
|
| + throw new IllegalStateException(e);
|
| + } catch (InterruptedException e) {
|
| + throw new RuntimeException(e);
|
| + } catch (IOException e) {
|
| + throw new RuntimeException(e);
|
| + } catch (NoSuchAlgorithmException e) {
|
| + throw new IllegalStateException(e);
|
| + } catch (KeyManagementException e) {
|
| + throw new IllegalStateException(e);
|
| + }
|
| +
|
| + sInstance = null;
|
| + }
|
| +
|
| + /**
|
| + * Sets a response to be returned when a particular request path is passed
|
| + * in (with the option to specify additional headers).
|
| + *
|
| + * @param requestPath The path to respond to.
|
| + * @param resposneString The response body that will be returned.
|
| + * @param responseHeaders Any additional headers that should be returned along with the
|
| + * response (null is acceptable).
|
| + * @return The full URL including the path that should be requested to get the expected
|
| + * response.
|
| + */
|
| + public String setResponse(
|
| + String requestPath, String resposneString,
|
| + List<Pair<String, String>> responseHeaders) {
|
| + mResponseMap.put(requestPath, new Response(resposneString, responseHeaders));
|
| + return mServerUri + requestPath;
|
| + }
|
| +
|
| + private URLConnection openConnection(URL url)
|
| + throws IOException, NoSuchAlgorithmException, KeyManagementException {
|
| + if (mSsl) {
|
| + // Install hostname verifiers and trust managers that don't do
|
| + // anything in order to get around the client not trusting
|
| + // the test server due to a lack of certificates.
|
| +
|
| + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
| + connection.setHostnameVerifier(new TestHostnameVerifier());
|
| +
|
| + SSLContext context = SSLContext.getInstance("TLS");
|
| + TestTrustManager trustManager = new TestTrustManager();
|
| + context.init(null, new TestTrustManager[] {trustManager}, null);
|
| + connection.setSSLSocketFactory(context.getSocketFactory());
|
| +
|
| + return connection;
|
| + } else {
|
| + return url.openConnection();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * {@link X509TrustManager} that trusts everybody. This is used so that
|
| + * the client calling {@link TestWebServer#shutdown()} can issue a request
|
| + * for shutdown by blindly trusting the {@link TestWebServer}'s
|
| + * credentials.
|
| + */
|
| + private static class TestTrustManager implements X509TrustManager {
|
| + @Override
|
| + public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
| + // Trust the TestWebServer...
|
| + }
|
| +
|
| + @Override
|
| + public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
| + // Trust the TestWebServer...
|
| + }
|
| +
|
| + @Override
|
| + public X509Certificate[] getAcceptedIssuers() {
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * {@link HostnameVerifier} that verifies everybody. This permits
|
| + * the client to trust the web server and call
|
| + * {@link TestWebServer#shutdown()}.
|
| + */
|
| + private static class TestHostnameVerifier implements HostnameVerifier {
|
| + @Override
|
| + public boolean verify(String hostname, SSLSession session) {
|
| + return true;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Generate a response to the given request.
|
| + * @throws InterruptedException
|
| + */
|
| + private HttpResponse getResponse(HttpRequest request) throws InterruptedException {
|
| + RequestLine requestLine = request.getRequestLine();
|
| + HttpResponse httpResponse = null;
|
| + Log.i(TAG, requestLine.getMethod() + ": " + requestLine.getUri());
|
| + String uriString = requestLine.getUri();
|
| + URI uri = URI.create(uriString);
|
| + String path = uri.getPath();
|
| +
|
| + Response response = mResponseMap.get(path);
|
| + if (path.equals(SHUTDOWN_PREFIX)) {
|
| + httpResponse = createResponse(HttpStatus.SC_OK);
|
| + } else if (response == null) {
|
| + httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
|
| + } else {
|
| + httpResponse = createResponse(HttpStatus.SC_OK);
|
| + httpResponse.setEntity(createEntity(response.mResponseStr));
|
| + for (Pair<String, String> header : response.mResponseHeaders) {
|
| + httpResponse.addHeader(header.first, header.second);
|
| + }
|
| + }
|
| + StatusLine sl = httpResponse.getStatusLine();
|
| + Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")");
|
| + setDateHeaders(httpResponse);
|
| + return httpResponse;
|
| + }
|
| +
|
| + private void setDateHeaders(HttpResponse response) {
|
| + long time = System.currentTimeMillis();
|
| + response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123));
|
| + }
|
| +
|
| + /**
|
| + * Create an empty response with the given status.
|
| + */
|
| + private HttpResponse createResponse(int status) {
|
| + HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null);
|
| +
|
| + if (sReasons == null) {
|
| + sReasons = new Hashtable<Integer, String>();
|
| + sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized");
|
| + sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found");
|
| + sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden");
|
| + sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily");
|
| + }
|
| + // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is Locale-dependent.
|
| + String reason = sReasons.get(status);
|
| +
|
| + if (reason != null) {
|
| + StringBuffer buf = new StringBuffer("<html><head><title>");
|
| + buf.append(reason);
|
| + buf.append("</title></head><body>");
|
| + buf.append(reason);
|
| + buf.append("</body></html>");
|
| + response.setEntity(createEntity(buf.toString()));
|
| + }
|
| + return response;
|
| + }
|
| +
|
| + /**
|
| + * Create a string entity for the given content.
|
| + */
|
| + private StringEntity createEntity(String content) {
|
| + try {
|
| + StringEntity entity = new StringEntity(content);
|
| + entity.setContentType("text/html");
|
| + return entity;
|
| + } catch (UnsupportedEncodingException e) {
|
| + Log.w(TAG, e);
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + private static class ServerThread extends Thread {
|
| + private TestWebServer mServer;
|
| + private ServerSocket mSocket;
|
| + private boolean mIsSsl;
|
| + private boolean mIsCancelled;
|
| + private SSLContext mSslContext;
|
| +
|
| + /**
|
| + * Defines the keystore contents for the server, BKS version. Holds just a
|
| + * single self-generated key. The subject name is "Test Server".
|
| + */
|
| + private static final String SERVER_KEYS_BKS =
|
| + "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";
|
| +
|
| + private String PASSWORD = "android";
|
| +
|
| + /**
|
| + * Loads a keystore from a base64-encoded String. Returns the KeyManager[]
|
| + * for the result.
|
| + */
|
| + private KeyManager[] getKeyManagers() throws Exception {
|
| + byte[] bytes = Base64.decode(SERVER_KEYS_BKS, Base64.DEFAULT);
|
| + InputStream inputStream = new ByteArrayInputStream(bytes);
|
| +
|
| + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
| + keyStore.load(inputStream, PASSWORD.toCharArray());
|
| + inputStream.close();
|
| +
|
| + String algorithm = KeyManagerFactory.getDefaultAlgorithm();
|
| + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
|
| + keyManagerFactory.init(keyStore, PASSWORD.toCharArray());
|
| +
|
| + return keyManagerFactory.getKeyManagers();
|
| + }
|
| +
|
| +
|
| + public ServerThread(TestWebServer server, boolean ssl) throws Exception {
|
| + super("ServerThread");
|
| + mServer = server;
|
| + mIsSsl = ssl;
|
| + int retry = 3;
|
| + while (true) {
|
| + try {
|
| + if (mIsSsl) {
|
| + mSslContext = SSLContext.getInstance("TLS");
|
| + mSslContext.init(getKeyManagers(), null, null);
|
| + mSocket = mSslContext.getServerSocketFactory().createServerSocket(
|
| + SSL_SERVER_PORT);
|
| + } else {
|
| + mSocket = new ServerSocket(SERVER_PORT);
|
| + }
|
| + return;
|
| + } catch (IOException e) {
|
| + Log.w(TAG, e);
|
| + if (--retry == 0) {
|
| + throw e;
|
| + }
|
| + // sleep in case server socket is still being closed
|
| + Thread.sleep(1000);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void run() {
|
| + HttpParams params = new BasicHttpParams();
|
| + params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0);
|
| + while (!mIsCancelled) {
|
| + try {
|
| + Socket socket = mSocket.accept();
|
| + DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
|
| + conn.bind(socket, params);
|
| +
|
| + // Determine whether we need to shutdown early before
|
| + // parsing the response since conn.close() will crash
|
| + // for SSL requests due to UnsupportedOperationException.
|
| + HttpRequest request = conn.receiveRequestHeader();
|
| + if (isShutdownRequest(request)) {
|
| + mIsCancelled = true;
|
| + }
|
| +
|
| + HttpResponse response = mServer.getResponse(request);
|
| + conn.sendResponseHeader(response);
|
| + conn.sendResponseEntity(response);
|
| + conn.close();
|
| +
|
| + } catch (IOException e) {
|
| + // normal during shutdown, ignore
|
| + Log.w(TAG, e);
|
| + } catch (HttpException e) {
|
| + Log.w(TAG, e);
|
| + } catch (InterruptedException e) {
|
| + Log.w(TAG, e);
|
| + } catch (UnsupportedOperationException e) {
|
| + // DefaultHttpServerConnection's close() throws an
|
| + // UnsupportedOperationException.
|
| + Log.w(TAG, e);
|
| + }
|
| + }
|
| + try {
|
| + mSocket.close();
|
| + } catch (IOException ignored) {
|
| + // safe to ignore
|
| + }
|
| + }
|
| +
|
| + private boolean isShutdownRequest(HttpRequest request) {
|
| + RequestLine requestLine = request.getRequestLine();
|
| + String uriString = requestLine.getUri();
|
| + URI uri = URI.create(uriString);
|
| + String path = uri.getPath();
|
| + return path.equals(SHUTDOWN_PREFIX);
|
| + }
|
| + }
|
| +}
|
|
|