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