1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.net.test.util;
7 import android.util.Base64;
8 import android.util.Log;
9 import android.util.Pair;
11 import org.apache.http.HttpException;
12 import org.apache.http.HttpRequest;
13 import org.apache.http.HttpResponse;
14 import org.apache.http.HttpStatus;
15 import org.apache.http.HttpVersion;
16 import org.apache.http.RequestLine;
17 import org.apache.http.StatusLine;
18 import org.apache.http.entity.ByteArrayEntity;
19 import org.apache.http.impl.DefaultHttpServerConnection;
20 import org.apache.http.impl.cookie.DateUtils;
21 import org.apache.http.message.BasicHttpResponse;
22 import org.apache.http.params.BasicHttpParams;
23 import org.apache.http.params.CoreProtocolPNames;
24 import org.apache.http.params.HttpParams;
26 import java.io.ByteArrayInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.net.MalformedURLException;
30 import java.net.ServerSocket;
31 import java.net.Socket;
34 import java.net.URLConnection;
35 import java.security.KeyManagementException;
36 import java.security.KeyStore;
37 import java.security.NoSuchAlgorithmException;
38 import java.security.cert.X509Certificate;
39 import java.util.ArrayList;
40 import java.util.Date;
41 import java.util.HashMap;
42 import java.util.Hashtable;
43 import java.util.List;
46 import javax.net.ssl.HostnameVerifier;
47 import javax.net.ssl.HttpsURLConnection;
48 import javax.net.ssl.KeyManager;
49 import javax.net.ssl.KeyManagerFactory;
50 import javax.net.ssl.SSLContext;
51 import javax.net.ssl.SSLSession;
52 import javax.net.ssl.X509TrustManager;
55 * Simple http test server for testing.
57 * This server runs in a thread in the current process, so it is convenient
58 * for loopback testing without the need to setup tcp forwarding to the
61 * Based heavily on the CTSWebServer in Android.
63 public class TestWebServer {
64 private static final String TAG = "TestWebServer";
66 public static final String SHUTDOWN_PREFIX = "/shutdown";
68 private static TestWebServer sInstance;
69 private static TestWebServer sSecureInstance;
70 private static Hashtable<Integer, String> sReasons;
72 private final ServerThread mServerThread;
73 private String mServerUri;
74 private final boolean mSsl;
75 private final int mPort;
77 private static class Response {
78 final byte[] mResponseData;
79 final List<Pair<String, String>> mResponseHeaders;
80 final boolean mIsRedirect;
81 final Runnable mResponseAction;
82 final boolean mIsNotFound;
84 Response(byte[] responseData, List<Pair<String, String>> responseHeaders,
85 boolean isRedirect, boolean isNotFound, Runnable responseAction) {
86 mIsRedirect = isRedirect;
87 mIsNotFound = isNotFound;
88 mResponseData = responseData;
89 mResponseHeaders = responseHeaders == null ?
90 new ArrayList<Pair<String, String>>() : responseHeaders;
91 mResponseAction = responseAction;
95 // The Maps below are modified on both the client thread and the internal server thread, so
96 // need to use a lock when accessing them.
97 private final Object mLock = new Object();
98 private final Map<String, Response> mResponseMap = new HashMap<String, Response>();
99 private final Map<String, Integer> mResponseCountMap = new HashMap<String, Integer>();
100 private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>();
103 * Create and start a local HTTP server instance.
104 * @param port Port number the server must use, or 0 to automatically choose a free port.
105 * @param ssl True if the server should be using secure sockets.
108 private TestWebServer(int port, boolean ssl) throws Exception {
113 mServerUri = "https:";
114 if (sSecureInstance != null) {
115 sSecureInstance.shutdown();
118 mServerUri = "http:";
119 if (sInstance != null) {
120 sInstance.shutdown();
124 mServerThread = new ServerThread(this, mPort, mSsl);
125 mServerUri += "//localhost:" + mServerThread.mSocket.getLocalPort();
128 public static TestWebServer start(int port) throws Exception {
129 if (sInstance != null) {
130 throw new IllegalStateException("Tried to start multiple TestWebServers");
133 TestWebServer server = new TestWebServer(port, false);
134 server.mServerThread.start();
139 public static TestWebServer start() throws Exception {
143 public static TestWebServer startSsl(int port) throws Exception {
144 if (sSecureInstance != null) {
145 throw new IllegalStateException("Tried to start multiple SSL TestWebServers");
148 TestWebServer server = new TestWebServer(port, true);
149 server.mServerThread.start();
150 setSecureInstance(server);
154 public static TestWebServer startSsl() throws Exception {
159 * Terminate the http server.
161 public void shutdown() {
163 setSecureInstance(null);
169 // Avoid a deadlock between two threads where one is trying to call
170 // close() and the other one is calling accept() by sending a GET
171 // request for shutdown and having the server's one thread
172 // sequentially call accept() and close().
173 URL url = new URL(mServerUri + SHUTDOWN_PREFIX);
174 URLConnection connection = openConnection(url);
175 connection.connect();
177 // Read the input from the stream to send the request.
178 InputStream is = connection.getInputStream();
181 // Block until the server thread is done shutting down.
182 mServerThread.join();
184 } catch (MalformedURLException e) {
185 throw new IllegalStateException(e);
186 } catch (InterruptedException e) {
187 throw new RuntimeException(e);
188 } catch (IOException e) {
189 throw new RuntimeException(e);
190 } catch (NoSuchAlgorithmException e) {
191 throw new IllegalStateException(e);
192 } catch (KeyManagementException e) {
193 throw new IllegalStateException(e);
197 // Setting static variables from instance methods causes findbugs warnings. Calling static
198 // methods which set static variables from instance methods isn't any better, but it silences
200 private static void setInstance(TestWebServer instance) {
201 sInstance = instance;
204 private static void setSecureInstance(TestWebServer instance) {
205 sSecureInstance = instance;
208 private static final int RESPONSE_STATUS_NORMAL = 0;
209 private static final int RESPONSE_STATUS_MOVED_TEMPORARILY = 1;
210 private static final int RESPONSE_STATUS_NOT_FOUND = 2;
212 private String setResponseInternal(
213 String requestPath, byte[] responseData,
214 List<Pair<String, String>> responseHeaders, Runnable responseAction,
216 final boolean isRedirect = (status == RESPONSE_STATUS_MOVED_TEMPORARILY);
217 final boolean isNotFound = (status == RESPONSE_STATUS_NOT_FOUND);
219 synchronized (mLock) {
220 mResponseMap.put(requestPath, new Response(
221 responseData, responseHeaders, isRedirect, isNotFound, responseAction));
222 mResponseCountMap.put(requestPath, Integer.valueOf(0));
223 mLastRequestMap.put(requestPath, null);
225 return getResponseUrl(requestPath);
229 * Gets the URL on the server under which a particular request path will be accessible.
231 * This only gets the URL, you still need to set the response if you intend to access it.
233 * @param requestPath The path to respond to.
234 * @return The full URL including the requestPath.
236 public String getResponseUrl(String requestPath) {
237 return mServerUri + requestPath;
241 * Sets a 404 (not found) response to be returned when a particular request path is passed in.
243 * @param requestPath The path to respond to.
244 * @return The full URL including the path that should be requested to get the expected
247 public String setResponseWithNotFoundStatus(
248 String requestPath) {
249 return setResponseInternal(requestPath, "".getBytes(), null, null,
250 RESPONSE_STATUS_NOT_FOUND);
254 * Sets a response to be returned when a particular request path is passed
255 * in (with the option to specify additional headers).
257 * @param requestPath The path to respond to.
258 * @param responseString The response body that will be returned.
259 * @param responseHeaders Any additional headers that should be returned along with the
260 * response (null is acceptable).
261 * @return The full URL including the path that should be requested to get the expected
264 public String setResponse(
265 String requestPath, String responseString,
266 List<Pair<String, String>> responseHeaders) {
267 return setResponseInternal(requestPath, responseString.getBytes(), responseHeaders, null,
268 RESPONSE_STATUS_NORMAL);
272 * Sets a response to be returned when a particular request path is passed
273 * in with the option to specify additional headers as well as an arbitrary action to be
274 * executed on each request.
276 * @param requestPath The path to respond to.
277 * @param responseString The response body that will be returned.
278 * @param responseHeaders Any additional headers that should be returned along with the
279 * response (null is acceptable).
280 * @param responseAction The action to be performed when fetching the response. This action
281 * will be executed for each request and will be handled on a background
283 * @return The full URL including the path that should be requested to get the expected
286 public String setResponseWithRunnableAction(
287 String requestPath, String responseString, List<Pair<String, String>> responseHeaders,
288 Runnable responseAction) {
289 return setResponseInternal(
290 requestPath, responseString.getBytes(), responseHeaders, responseAction,
291 RESPONSE_STATUS_NORMAL);
297 * @param requestPath The path to respond to.
298 * @param targetPath The path to redirect to.
299 * @return The full URL including the path that should be requested to get the expected
302 public String setRedirect(
303 String requestPath, String targetPath) {
304 List<Pair<String, String>> responseHeaders = new ArrayList<Pair<String, String>>();
305 responseHeaders.add(Pair.create("Location", targetPath));
307 return setResponseInternal(requestPath, targetPath.getBytes(), responseHeaders, null,
308 RESPONSE_STATUS_MOVED_TEMPORARILY);
312 * Sets a base64 encoded response to be returned when a particular request path is passed
313 * in (with the option to specify additional headers).
315 * @param requestPath The path to respond to.
316 * @param base64EncodedResponse The response body that is base64 encoded. The actual server
317 * response will the decoded binary form.
318 * @param responseHeaders Any additional headers that should be returned along with the
319 * response (null is acceptable).
320 * @return The full URL including the path that should be requested to get the expected
323 public String setResponseBase64(
324 String requestPath, String base64EncodedResponse,
325 List<Pair<String, String>> responseHeaders) {
326 return setResponseInternal(
327 requestPath, Base64.decode(base64EncodedResponse, Base64.DEFAULT),
328 responseHeaders, null, RESPONSE_STATUS_NORMAL);
332 * Get the number of requests was made at this path since it was last set.
334 public int getRequestCount(String requestPath) {
335 Integer count = null;
336 synchronized (mLock) {
337 count = mResponseCountMap.get(requestPath);
339 if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath);
340 return count.intValue();
344 * Returns the last HttpRequest at this path. Can return null if it is never requested.
346 public HttpRequest getLastRequest(String requestPath) {
347 synchronized (mLock) {
348 if (!mLastRequestMap.containsKey(requestPath))
349 throw new IllegalArgumentException("Path not set: " + requestPath);
350 return mLastRequestMap.get(requestPath);
354 public String getBaseUrl() {
355 return mServerUri + "/";
358 private URLConnection openConnection(URL url)
359 throws IOException, NoSuchAlgorithmException, KeyManagementException {
361 // Install hostname verifiers and trust managers that don't do
362 // anything in order to get around the client not trusting
363 // the test server due to a lack of certificates.
365 HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
366 connection.setHostnameVerifier(new TestHostnameVerifier());
368 SSLContext context = SSLContext.getInstance("TLS");
369 TestTrustManager trustManager = new TestTrustManager();
370 context.init(null, new TestTrustManager[] {trustManager}, null);
371 connection.setSSLSocketFactory(context.getSocketFactory());
375 return url.openConnection();
380 * {@link X509TrustManager} that trusts everybody. This is used so that
381 * the client calling {@link TestWebServer#shutdown()} can issue a request
382 * for shutdown by blindly trusting the {@link TestWebServer}'s
385 private static class TestTrustManager implements X509TrustManager {
387 public void checkClientTrusted(X509Certificate[] chain, String authType) {
388 // Trust the TestWebServer...
392 public void checkServerTrusted(X509Certificate[] chain, String authType) {
393 // Trust the TestWebServer...
397 public X509Certificate[] getAcceptedIssuers() {
403 * {@link HostnameVerifier} that verifies everybody. This permits
404 * the client to trust the web server and call
405 * {@link TestWebServer#shutdown()}.
407 private static class TestHostnameVerifier implements HostnameVerifier {
409 public boolean verify(String hostname, SSLSession session) {
414 private void servedResponseFor(String path, HttpRequest request) {
415 synchronized (mLock) {
416 mResponseCountMap.put(path, Integer.valueOf(
417 mResponseCountMap.get(path).intValue() + 1));
418 mLastRequestMap.put(path, request);
423 * Generate a response to the given request.
425 * <p>Always executed on the background server thread.
427 * <p>If there is an action associated with the response, it will be executed inside of
430 * @throws InterruptedException
432 private HttpResponse getResponse(HttpRequest request) throws InterruptedException {
433 assert Thread.currentThread() == mServerThread
434 : "getResponse called from non-server thread";
436 RequestLine requestLine = request.getRequestLine();
437 HttpResponse httpResponse = null;
438 Log.i(TAG, requestLine.getMethod() + ": " + requestLine.getUri());
439 String uriString = requestLine.getUri();
440 URI uri = URI.create(uriString);
441 String path = uri.getPath();
443 Response response = null;
444 synchronized (mLock) {
445 response = mResponseMap.get(path);
447 if (path.equals(SHUTDOWN_PREFIX)) {
448 httpResponse = createResponse(HttpStatus.SC_OK);
449 } else if (response == null) {
450 httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
451 } else if (response.mIsNotFound) {
452 httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
453 servedResponseFor(path, request);
454 } else if (response.mIsRedirect) {
455 httpResponse = createResponse(HttpStatus.SC_MOVED_TEMPORARILY);
456 for (Pair<String, String> header : response.mResponseHeaders) {
457 httpResponse.addHeader(header.first, header.second);
459 servedResponseFor(path, request);
461 if (response.mResponseAction != null) response.mResponseAction.run();
463 httpResponse = createResponse(HttpStatus.SC_OK);
464 ByteArrayEntity entity = createEntity(response.mResponseData);
465 httpResponse.setEntity(entity);
466 httpResponse.setHeader("Content-Length", "" + entity.getContentLength());
467 for (Pair<String, String> header : response.mResponseHeaders) {
468 httpResponse.addHeader(header.first, header.second);
470 servedResponseFor(path, request);
472 StatusLine sl = httpResponse.getStatusLine();
473 Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")");
474 setDateHeaders(httpResponse);
478 private void setDateHeaders(HttpResponse response) {
479 response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123));
483 * Create an empty response with the given status.
485 private HttpResponse createResponse(int status) {
486 HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null);
487 String reason = null;
489 // This synchronized silences findbugs.
490 synchronized (TestWebServer.class) {
491 if (sReasons == null) {
492 sReasons = new Hashtable<Integer, String>();
493 sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized");
494 sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found");
495 sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden");
496 sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily");
498 // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is
500 reason = sReasons.get(status);
503 if (reason != null) {
504 StringBuffer buf = new StringBuffer("<html><head><title>");
506 buf.append("</title></head><body>");
508 buf.append("</body></html>");
509 ByteArrayEntity entity = createEntity(buf.toString().getBytes());
510 response.setEntity(entity);
511 response.setHeader("Content-Length", "" + entity.getContentLength());
517 * Create a string entity for the given content.
519 private ByteArrayEntity createEntity(byte[] data) {
520 ByteArrayEntity entity = new ByteArrayEntity(data);
521 entity.setContentType("text/html");
525 private static class ServerThread extends Thread {
526 private TestWebServer mServer;
527 private ServerSocket mSocket;
528 private boolean mIsSsl;
529 private boolean mIsCancelled;
530 private SSLContext mSslContext;
533 * Defines the keystore contents for the server, BKS version. Holds just a
534 * single self-generated key. The subject name is "Test Server".
536 private static final String SERVER_KEYS_BKS =
537 "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" +
538 "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" +
539 "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" +
540 "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" +
541 "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" +
542 "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" +
543 "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" +
544 "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" +
545 "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" +
546 "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" +
547 "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" +
548 "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" +
549 "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" +
550 "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" +
551 "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" +
552 "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" +
553 "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" +
554 "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" +
555 "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" +
556 "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" +
557 "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" +
558 "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" +
559 "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" +
560 "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw=";
562 private static final String PASSWORD = "android";
565 * Loads a keystore from a base64-encoded String. Returns the KeyManager[]
568 private KeyManager[] getKeyManagers() throws Exception {
569 byte[] bytes = Base64.decode(SERVER_KEYS_BKS, Base64.DEFAULT);
570 InputStream inputStream = new ByteArrayInputStream(bytes);
572 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
573 keyStore.load(inputStream, PASSWORD.toCharArray());
576 String algorithm = KeyManagerFactory.getDefaultAlgorithm();
577 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
578 keyManagerFactory.init(keyStore, PASSWORD.toCharArray());
580 return keyManagerFactory.getKeyManagers();
584 public ServerThread(TestWebServer server, int port, boolean ssl) throws Exception {
585 super("ServerThread");
592 mSslContext = SSLContext.getInstance("TLS");
593 mSslContext.init(getKeyManagers(), null, null);
594 mSocket = mSslContext.getServerSocketFactory().createServerSocket(port);
596 mSocket = new ServerSocket(port);
599 } catch (IOException e) {
604 // sleep in case server socket is still being closed
612 HttpParams params = new BasicHttpParams();
613 params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0);
614 while (!mIsCancelled) {
616 Socket socket = mSocket.accept();
617 DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
618 conn.bind(socket, params);
620 // Determine whether we need to shutdown early before
621 // parsing the response since conn.close() will crash
622 // for SSL requests due to UnsupportedOperationException.
623 HttpRequest request = conn.receiveRequestHeader();
624 if (isShutdownRequest(request)) {
628 HttpResponse response = mServer.getResponse(request);
629 conn.sendResponseHeader(response);
630 conn.sendResponseEntity(response);
633 } catch (IOException e) {
634 // normal during shutdown, ignore
636 } catch (HttpException e) {
638 } catch (InterruptedException e) {
640 } catch (UnsupportedOperationException e) {
641 // DefaultHttpServerConnection's close() throws an
642 // UnsupportedOperationException.
648 } catch (IOException ignored) {
653 private boolean isShutdownRequest(HttpRequest request) {
654 RequestLine requestLine = request.getRequestLine();
655 String uriString = requestLine.getUri();
656 URI uri = URI.create(uriString);
657 String path = uri.getPath();
658 return path.equals(SHUTDOWN_PREFIX);