1fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Manton// Copyright 2012 The Chromium Authors. All rights reserved.
2d18bd04dde2aca78afd8cec4d9dc4b2fd172ad38Younes Manton// Use of this source code is governed by a BSD-style license that can be
3fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Manton// found in the LICENSE file.
4fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Manton
5d18bd04dde2aca78afd8cec4d9dc4b2fd172ad38Younes Mantonpackage org.chromium.net.test.util;
6fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Manton
7fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport android.util.Base64;
8fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport android.util.Log;
9fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport android.util.Pair;
10fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Manton
11fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.HttpException;
12fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.HttpRequest;
13d18bd04dde2aca78afd8cec4d9dc4b2fd172ad38Younes Mantonimport org.apache.http.HttpResponse;
14fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.HttpStatus;
15fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.HttpVersion;
16fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.RequestLine;
17d18bd04dde2aca78afd8cec4d9dc4b2fd172ad38Younes Mantonimport org.apache.http.StatusLine;
18fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.entity.ByteArrayEntity;
19fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.impl.DefaultHttpServerConnection;
20fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.impl.cookie.DateUtils;
21fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.message.BasicHttpResponse;
22fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.params.BasicHttpParams;
23fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.params.CoreProtocolPNames;
24fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport org.apache.http.params.HttpParams;
25d18bd04dde2aca78afd8cec4d9dc4b2fd172ad38Younes Manton
26fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport java.io.ByteArrayInputStream;
27fcb595c04f9ee275eae49b7bb7c61246671f5ce2Younes Mantonimport java.io.IOException;
28f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.io.InputStream;
29f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.net.MalformedURLException;
30f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.net.ServerSocket;
3151ecb33c6b218af8e7c401a701b51f52331a1782Kai Wasserbächimport java.net.Socket;
3251ecb33c6b218af8e7c401a701b51f52331a1782Kai Wasserbächimport java.net.URI;
3351ecb33c6b218af8e7c401a701b51f52331a1782Kai Wasserbächimport java.net.URL;
3451ecb33c6b218af8e7c401a701b51f52331a1782Kai Wasserbächimport java.net.URLConnection;
3551ecb33c6b218af8e7c401a701b51f52331a1782Kai Wasserbächimport java.security.KeyManagementException;
36f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.security.KeyStore;
37f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.security.NoSuchAlgorithmException;
38f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.security.cert.X509Certificate;
39f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.util.ArrayList;
40f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Mantonimport java.util.Date;
41835ea8480f656ba4076e30813eb8c85965017266Christian Königimport java.util.HashMap;
429765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport java.util.Hashtable;
439765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport java.util.List;
449765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport java.util.Map;
459765dede7556f7ccfef1d90bab14a2bfa03384e5Christian König
469765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.HostnameVerifier;
479765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.HttpsURLConnection;
489765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.KeyManager;
499765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.KeyManagerFactory;
509765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.SSLContext;
519765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.SSLSession;
529765dede7556f7ccfef1d90bab14a2bfa03384e5Christian Königimport javax.net.ssl.X509TrustManager;
53835ea8480f656ba4076e30813eb8c85965017266Christian König
54835ea8480f656ba4076e30813eb8c85965017266Christian König/**
55835ea8480f656ba4076e30813eb8c85965017266Christian König * Simple http test server for testing.
56f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton *
57835ea8480f656ba4076e30813eb8c85965017266Christian König * This server runs in a thread in the current process, so it is convenient
58835ea8480f656ba4076e30813eb8c85965017266Christian König * for loopback testing without the need to setup tcp forwarding to the
59835ea8480f656ba4076e30813eb8c85965017266Christian König * host computer.
60835ea8480f656ba4076e30813eb8c85965017266Christian König *
61f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton * Based heavily on the CTSWebServer in Android.
62f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton */
63d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian Königpublic final class TestWebServer {
64d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static final String TAG = "TestWebServer";
65d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
66d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    public static final String SHUTDOWN_PREFIX = "/shutdown";
672e6274fc3b123e7de695038054b5cbd20b11559aChristian König
68d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static TestWebServer sInstance;
69d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static TestWebServer sSecureInstance;
70d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static Hashtable<Integer, String> sReasons;
71d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
72d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private final ServerThread mServerThread;
732e6274fc3b123e7de695038054b5cbd20b11559aChristian König    private String mServerUri;
742e6274fc3b123e7de695038054b5cbd20b11559aChristian König    private final boolean mSsl;
75d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
76d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static class Response {
77d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        final byte[] mResponseData;
78d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        final List<Pair<String, String>> mResponseHeaders;
79f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        final boolean mIsRedirect;
80d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        final Runnable mResponseAction;
81d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        final boolean mIsNotFound;
82d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
83d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        Response(byte[] responseData, List<Pair<String, String>> responseHeaders,
84d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König                boolean isRedirect, boolean isNotFound, Runnable responseAction) {
85f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton            mIsRedirect = isRedirect;
86f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton            mIsNotFound = isNotFound;
87d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            mResponseData = responseData;
88d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            mResponseHeaders = responseHeaders == null ?
89d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König                    new ArrayList<Pair<String, String>>() : responseHeaders;
90d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            mResponseAction = responseAction;
91b7acf83d523563cde613fe805bd8edaa02f64b53Christian König        }
92d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    }
93d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
94b7acf83d523563cde613fe805bd8edaa02f64b53Christian König    // The Maps below are modified on both the client thread and the internal server thread, so
95b7acf83d523563cde613fe805bd8edaa02f64b53Christian König    // need to use a lock when accessing them.
96d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private final Object mLock = new Object();
97bac8760f7f3523e9b6d5b2fd7cd46091d4883f5eChristian König    private final Map<String, Response> mResponseMap = new HashMap<String, Response>();
98d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private final Map<String, Integer> mResponseCountMap = new HashMap<String, Integer>();
99d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>();
100d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
101d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    /**
102bac8760f7f3523e9b6d5b2fd7cd46091d4883f5eChristian König     * Create and start a local HTTP server instance.
103bac8760f7f3523e9b6d5b2fd7cd46091d4883f5eChristian König     * @param ssl True if the server should be using secure sockets.
104d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König     * @throws Exception
105f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton     */
106d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    public TestWebServer(boolean ssl) throws Exception {
107f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        mSsl = ssl;
108f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        if (mSsl) {
1092e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König            mServerUri = "https:";
1102e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König            if (sSecureInstance != null) {
1112e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König                sSecureInstance.shutdown();
1122e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König            }
1132e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König        } else {
114d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            mServerUri = "http:";
115f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton            if (sInstance != null) {
116d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König                sInstance.shutdown();
117f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton            }
118f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        }
119f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton
120f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        setInstance(this, mSsl);
121f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        mServerThread = new ServerThread(this, mSsl);
122f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        mServerThread.start();
123f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton        mServerUri += "//localhost:" + mServerThread.mSocket.getLocalPort();
124f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton    }
125f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton
126f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton    /**
127f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton     * Terminate the http server.
128c888fe027c338f337123de4da2de1ac73b0f7587Christian König     */
129c888fe027c338f337123de4da2de1ac73b0f7587Christian König    public void shutdown() {
1302e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König        try {
131c888fe027c338f337123de4da2de1ac73b0f7587Christian König            // Avoid a deadlock between two threads where one is trying to call
13245bb4b79f6ddd1fdb447632c88333866585fb80cMaarten Lankhorst            // close() and the other one is calling accept() by sending a GET
13345bb4b79f6ddd1fdb447632c88333866585fb80cMaarten Lankhorst            // request for shutdown and having the server's one thread
13445bb4b79f6ddd1fdb447632c88333866585fb80cMaarten Lankhorst            // sequentially call accept() and close().
135c4d47f065ae2a015a9d2e9a060d71e04d5935c2bMaarten Lankhorst            URL url = new URL(mServerUri + SHUTDOWN_PREFIX);
1362e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König            URLConnection connection = openConnection(url);
1372e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König            connection.connect();
1382e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König
1398ea416f35de0c664ef47b71841756758f22d7faaChristian König            // Read the input from the stream to send the request.
1408ea416f35de0c664ef47b71841756758f22d7faaChristian König            InputStream is = connection.getInputStream();
1412e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König            is.close();
1422e62b30826679e9d5e1a783dc19baabec4fc8dfaChristian König
143d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            // Block until the server thread is done shutting down.
144d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            mServerThread.join();
145d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
146d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        } catch (MalformedURLException e) {
147d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            throw new IllegalStateException(e);
148d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        } catch (InterruptedException e) {
149d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            throw new RuntimeException(e);
150d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        } catch (IOException e) {
151d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            throw new RuntimeException(e);
152d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        } catch (NoSuchAlgorithmException e) {
153d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            throw new IllegalStateException(e);
154d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        } catch (KeyManagementException e) {
155d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            throw new IllegalStateException(e);
156d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        }
157d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
158d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        setInstance(null, mSsl);
159d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    }
160d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
161d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static void setInstance(TestWebServer instance, boolean isSsl) {
162d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        if (isSsl) {
163d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            sSecureInstance = instance;
164d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        } else {
165d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            sInstance = instance;
166d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        }
167d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    }
168d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
169d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static final int RESPONSE_STATUS_NORMAL = 0;
170d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static final int RESPONSE_STATUS_MOVED_TEMPORARILY = 1;
171d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private static final int RESPONSE_STATUS_NOT_FOUND = 2;
172d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
173d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König    private String setResponseInternal(
174d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            String requestPath, byte[] responseData,
175d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            List<Pair<String, String>> responseHeaders, Runnable responseAction,
176d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            int status) {
177d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        final boolean isRedirect = (status == RESPONSE_STATUS_MOVED_TEMPORARILY);
178d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        final boolean isNotFound = (status == RESPONSE_STATUS_NOT_FOUND);
179d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König
180d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König        synchronized (mLock) {
181d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König            mResponseMap.put(requestPath, new Response(
182d3770d6229d95e9beb67358ae2b2c8824ed3ae58Christian König                    responseData, responseHeaders, isRedirect, isNotFound, responseAction));
18312bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst            mResponseCountMap.put(requestPath, Integer.valueOf(0));
18412bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst            mLastRequestMap.put(requestPath, null);
18512bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst        }
1868ea416f35de0c664ef47b71841756758f22d7faaChristian König        return getResponseUrl(requestPath);
18712bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst    }
18812bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst
18912bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst    /**
19012bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     * Gets the URL on the server under which a particular request path will be accessible.
19112bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     *
19212bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     * This only gets the URL, you still need to set the response if you intend to access it.
19312bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     *
19412bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     * @param requestPath The path to respond to.
19512bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     * @return The full URL including the requestPath.
19612bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     */
19712bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst    public String getResponseUrl(String requestPath) {
19812bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst        return mServerUri + requestPath;
19912bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst    }
20012bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst
20112bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst    /**
20212bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     * Sets a 404 (not found) response to be returned when a particular request path is passed in.
20312bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     *
2048ea416f35de0c664ef47b71841756758f22d7faaChristian König     * @param requestPath The path to respond to.
2058ea416f35de0c664ef47b71841756758f22d7faaChristian König     * @return The full URL including the path that should be requested to get the expected
20612bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     *         response.
20712bf452945ae64da69f474298758f9a7c8b94af5Maarten Lankhorst     */
208eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst    public String setResponseWithNotFoundStatus(
209eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst            String requestPath) {
210eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst        return setResponseInternal(requestPath, "".getBytes(), null, null,
2118ea416f35de0c664ef47b71841756758f22d7faaChristian König                RESPONSE_STATUS_NOT_FOUND);
212eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst    }
213eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst
214eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst    /**
215eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * Sets a response to be returned when a particular request path is passed
216eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * in (with the option to specify additional headers).
217eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     *
218eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @param requestPath The path to respond to.
219eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @param responseString The response body that will be returned.
220eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @param responseHeaders Any additional headers that should be returned along with the
221eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     *                        response (null is acceptable).
222eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @return The full URL including the path that should be requested to get the expected
223eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     *         response.
224eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     */
225eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst    public String setResponse(
226eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst            String requestPath, String responseString,
227eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst            List<Pair<String, String>> responseHeaders) {
228eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst        return setResponseInternal(requestPath, responseString.getBytes(), responseHeaders, null,
229eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst                RESPONSE_STATUS_NORMAL);
230eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst    }
231eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst
232eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst    /**
233eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * Sets a response to be returned when a particular request path is passed
234eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * in with the option to specify additional headers as well as an arbitrary action to be
235eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * executed on each request.
236eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     *
237eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @param requestPath The path to respond to.
238eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @param responseString The response body that will be returned.
239eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @param responseHeaders Any additional headers that should be returned along with the
240eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     *                        response (null is acceptable).
2418ea416f35de0c664ef47b71841756758f22d7faaChristian König     * @param responseAction The action to be performed when fetching the response.  This action
2428ea416f35de0c664ef47b71841756758f22d7faaChristian König     *                       will be executed for each request and will be handled on a background
243eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     *                       thread.
244eadbcb221db16af96aa6c3f40d48896d23d9eebcMaarten Lankhorst     * @return The full URL including the path that should be requested to get the expected
245d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *         response.
246d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     */
247d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    public String setResponseWithRunnableAction(
248d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König            String requestPath, String responseString, List<Pair<String, String>> responseHeaders,
249d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König            Runnable responseAction) {
250d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König        return setResponseInternal(
251d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König                requestPath, responseString.getBytes(), responseHeaders, responseAction,
252d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König                RESPONSE_STATUS_NORMAL);
253d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    }
254d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König
255d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    /**
256d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * Sets a redirect.
257d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *
258d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @param requestPath The path to respond to.
259d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @param targetPath The path to redirect to.
260d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @return The full URL including the path that should be requested to get the expected
261d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *         response.
262d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     */
263d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    public String setRedirect(
264d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König            String requestPath, String targetPath) {
265d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König        List<Pair<String, String>> responseHeaders = new ArrayList<Pair<String, String>>();
266d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König        responseHeaders.add(Pair.create("Location", targetPath));
267d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König
268d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König        return setResponseInternal(requestPath, targetPath.getBytes(), responseHeaders, null,
269d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König                RESPONSE_STATUS_MOVED_TEMPORARILY);
270d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    }
271d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König
272d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    /**
273d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * Sets a base64 encoded response to be returned when a particular request path is passed
274d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * in (with the option to specify additional headers).
275d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *
276d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @param requestPath The path to respond to.
277d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @param base64EncodedResponse The response body that is base64 encoded. The actual server
278d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *                              response will the decoded binary form.
279d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @param responseHeaders Any additional headers that should be returned along with the
280d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *                        response (null is acceptable).
281d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     * @return The full URL including the path that should be requested to get the expected
282d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     *         response.
283d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König     */
284d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König    public String setResponseBase64(
285d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König            String requestPath, String base64EncodedResponse,
286d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König            List<Pair<String, String>> responseHeaders) {
287d6aa0ad55dc245bfacb7d9c3b479fe5a6557d43fChristian König        return setResponseInternal(
288f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton                requestPath, Base64.decode(base64EncodedResponse, Base64.DEFAULT),
289f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton                responseHeaders, null, RESPONSE_STATUS_NORMAL);
290f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton    }
291f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton
292f547472bfa0a797adacc2a7688b4c1ba65381a80Younes Manton    /**
293     * Get the number of requests was made at this path since it was last set.
294     */
295    public int getRequestCount(String requestPath) {
296        Integer count = null;
297        synchronized (mLock) {
298            count = mResponseCountMap.get(requestPath);
299        }
300        if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath);
301        return count.intValue();
302    }
303
304    /**
305     * Returns the last HttpRequest at this path. Can return null if it is never requested.
306     */
307    public HttpRequest getLastRequest(String requestPath) {
308        synchronized (mLock) {
309            if (!mLastRequestMap.containsKey(requestPath))
310                throw new IllegalArgumentException("Path not set: " + requestPath);
311            return mLastRequestMap.get(requestPath);
312        }
313    }
314
315    public String getBaseUrl() {
316        return mServerUri + "/";
317    }
318
319    private URLConnection openConnection(URL url)
320            throws IOException, NoSuchAlgorithmException, KeyManagementException {
321        if (mSsl) {
322            // Install hostname verifiers and trust managers that don't do
323            // anything in order to get around the client not trusting
324            // the test server due to a lack of certificates.
325
326            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
327            connection.setHostnameVerifier(new TestHostnameVerifier());
328
329            SSLContext context = SSLContext.getInstance("TLS");
330            TestTrustManager trustManager = new TestTrustManager();
331            context.init(null, new TestTrustManager[] {trustManager}, null);
332            connection.setSSLSocketFactory(context.getSocketFactory());
333
334            return connection;
335        } else {
336            return url.openConnection();
337        }
338    }
339
340    /**
341     * {@link X509TrustManager} that trusts everybody. This is used so that
342     * the client calling {@link TestWebServer#shutdown()} can issue a request
343     * for shutdown by blindly trusting the {@link TestWebServer}'s
344     * credentials.
345     */
346    private static class TestTrustManager implements X509TrustManager {
347        @Override
348        public void checkClientTrusted(X509Certificate[] chain, String authType) {
349            // Trust the TestWebServer...
350        }
351
352        @Override
353        public void checkServerTrusted(X509Certificate[] chain, String authType) {
354            // Trust the TestWebServer...
355        }
356
357        @Override
358        public X509Certificate[] getAcceptedIssuers() {
359            return null;
360        }
361    }
362
363    /**
364     * {@link HostnameVerifier} that verifies everybody. This permits
365     * the client to trust the web server and call
366     * {@link TestWebServer#shutdown()}.
367     */
368    private static class TestHostnameVerifier implements HostnameVerifier {
369        @Override
370        public boolean verify(String hostname, SSLSession session) {
371            return true;
372        }
373    }
374
375    private void servedResponseFor(String path, HttpRequest request) {
376        synchronized (mLock) {
377            mResponseCountMap.put(path, Integer.valueOf(
378                    mResponseCountMap.get(path).intValue() + 1));
379            mLastRequestMap.put(path, request);
380        }
381    }
382
383    /**
384     * Generate a response to the given request.
385     *
386     * <p>Always executed on the background server thread.
387     *
388     * <p>If there is an action associated with the response, it will be executed inside of
389     * this function.
390     *
391     * @throws InterruptedException
392     */
393    private HttpResponse getResponse(HttpRequest request) throws InterruptedException {
394        assert Thread.currentThread() == mServerThread
395                : "getResponse called from non-server thread";
396
397        RequestLine requestLine = request.getRequestLine();
398        HttpResponse httpResponse = null;
399        Log.i(TAG, requestLine.getMethod() + ": " + requestLine.getUri());
400        String uriString = requestLine.getUri();
401        URI uri = URI.create(uriString);
402        String path = uri.getPath();
403
404        Response response = null;
405        synchronized (mLock) {
406            response = mResponseMap.get(path);
407        }
408        if (path.equals(SHUTDOWN_PREFIX)) {
409            httpResponse = createResponse(HttpStatus.SC_OK);
410        } else if (response == null) {
411            httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
412        } else if (response.mIsNotFound) {
413            httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
414            servedResponseFor(path, request);
415        } else if (response.mIsRedirect) {
416            httpResponse = createResponse(HttpStatus.SC_MOVED_TEMPORARILY);
417            for (Pair<String, String> header : response.mResponseHeaders) {
418                httpResponse.addHeader(header.first, header.second);
419            }
420            servedResponseFor(path, request);
421        } else {
422            if (response.mResponseAction != null) response.mResponseAction.run();
423
424            httpResponse = createResponse(HttpStatus.SC_OK);
425            ByteArrayEntity entity = createEntity(response.mResponseData);
426            httpResponse.setEntity(entity);
427            httpResponse.setHeader("Content-Length", "" + entity.getContentLength());
428            for (Pair<String, String> header : response.mResponseHeaders) {
429                httpResponse.addHeader(header.first, header.second);
430            }
431            servedResponseFor(path, request);
432        }
433        StatusLine sl = httpResponse.getStatusLine();
434        Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")");
435        setDateHeaders(httpResponse);
436        return httpResponse;
437    }
438
439    private void setDateHeaders(HttpResponse response) {
440        response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123));
441    }
442
443    /**
444     * Create an empty response with the given status.
445     */
446    private HttpResponse createResponse(int status) {
447        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null);
448        String reason = null;
449
450        // This synchronized silences findbugs.
451        synchronized (TestWebServer.class) {
452            if (sReasons == null) {
453                sReasons = new Hashtable<Integer, String>();
454                sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized");
455                sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found");
456                sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden");
457                sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily");
458            }
459            // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is
460            // Locale-dependent.
461            reason = sReasons.get(status);
462        }
463
464        if (reason != null) {
465            StringBuffer buf = new StringBuffer("<html><head><title>");
466            buf.append(reason);
467            buf.append("</title></head><body>");
468            buf.append(reason);
469            buf.append("</body></html>");
470            ByteArrayEntity entity = createEntity(buf.toString().getBytes());
471            response.setEntity(entity);
472            response.setHeader("Content-Length", "" + entity.getContentLength());
473        }
474        return response;
475    }
476
477    /**
478     * Create a string entity for the given content.
479     */
480    private ByteArrayEntity createEntity(byte[] data) {
481        ByteArrayEntity entity = new ByteArrayEntity(data);
482        entity.setContentType("text/html");
483        return entity;
484    }
485
486    private static class ServerThread extends Thread {
487        private TestWebServer mServer;
488        private ServerSocket mSocket;
489        private boolean mIsSsl;
490        private boolean mIsCancelled;
491        private SSLContext mSslContext;
492
493        /**
494         * Defines the keystore contents for the server, BKS version. Holds just a
495         * single self-generated key. The subject name is "Test Server".
496         */
497        private static final String SERVER_KEYS_BKS =
498            "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" +
499            "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" +
500            "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" +
501            "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" +
502            "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" +
503            "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" +
504            "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" +
505            "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" +
506            "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" +
507            "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" +
508            "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" +
509            "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" +
510            "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" +
511            "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" +
512            "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" +
513            "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" +
514            "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" +
515            "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" +
516            "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" +
517            "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" +
518            "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" +
519            "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" +
520            "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" +
521            "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw=";
522
523        private static final String PASSWORD = "android";
524
525        /**
526         * Loads a keystore from a base64-encoded String. Returns the KeyManager[]
527         * for the result.
528         */
529        private KeyManager[] getKeyManagers() throws Exception {
530            byte[] bytes = Base64.decode(SERVER_KEYS_BKS, Base64.DEFAULT);
531            InputStream inputStream = new ByteArrayInputStream(bytes);
532
533            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
534            keyStore.load(inputStream, PASSWORD.toCharArray());
535            inputStream.close();
536
537            String algorithm = KeyManagerFactory.getDefaultAlgorithm();
538            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
539            keyManagerFactory.init(keyStore, PASSWORD.toCharArray());
540
541            return keyManagerFactory.getKeyManagers();
542        }
543
544
545        public ServerThread(TestWebServer server, boolean ssl) throws Exception {
546            super("ServerThread");
547            mServer = server;
548            mIsSsl = ssl;
549            int retry = 3;
550            while (true) {
551                try {
552                    if (mIsSsl) {
553                        mSslContext = SSLContext.getInstance("TLS");
554                        mSslContext.init(getKeyManagers(), null, null);
555                        mSocket = mSslContext.getServerSocketFactory().createServerSocket(0);
556                    } else {
557                        mSocket = new ServerSocket(0);
558                    }
559                    return;
560                } catch (IOException e) {
561                    Log.w(TAG, e);
562                    if (--retry == 0) {
563                        throw e;
564                    }
565                    // sleep in case server socket is still being closed
566                    Thread.sleep(1000);
567                }
568            }
569        }
570
571        @Override
572        public void run() {
573            HttpParams params = new BasicHttpParams();
574            params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0);
575            while (!mIsCancelled) {
576                try {
577                    Socket socket = mSocket.accept();
578                    DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
579                    conn.bind(socket, params);
580
581                    // Determine whether we need to shutdown early before
582                    // parsing the response since conn.close() will crash
583                    // for SSL requests due to UnsupportedOperationException.
584                    HttpRequest request = conn.receiveRequestHeader();
585                    if (isShutdownRequest(request)) {
586                        mIsCancelled = true;
587                    }
588
589                    HttpResponse response = mServer.getResponse(request);
590                    conn.sendResponseHeader(response);
591                    conn.sendResponseEntity(response);
592                    conn.close();
593
594                } catch (IOException e) {
595                    // normal during shutdown, ignore
596                    Log.w(TAG, e);
597                } catch (HttpException e) {
598                    Log.w(TAG, e);
599                } catch (InterruptedException e) {
600                    Log.w(TAG, e);
601                } catch (UnsupportedOperationException e) {
602                    // DefaultHttpServerConnection's close() throws an
603                    // UnsupportedOperationException.
604                    Log.w(TAG, e);
605                }
606            }
607            try {
608                mSocket.close();
609            } catch (IOException ignored) {
610                // safe to ignore
611            }
612        }
613
614        private boolean isShutdownRequest(HttpRequest request) {
615            RequestLine requestLine = request.getRequestLine();
616            String uriString = requestLine.getUri();
617            URI uri = URI.create(uriString);
618            String path = uri.getPath();
619            return path.equals(SHUTDOWN_PREFIX);
620        }
621    }
622}
623