• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2012 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.test.util;
6 
7 import android.util.Base64;
8 import android.util.Log;
9 import android.util.Pair;
10 
11 import org.chromium.base.ApiCompatibilityUtils;
12 
13 import java.io.IOException;
14 import java.io.OutputStream;
15 import java.io.PrintStream;
16 import java.nio.charset.Charset;
17 import java.security.MessageDigest;
18 import java.security.NoSuchAlgorithmException;
19 import java.text.SimpleDateFormat;
20 import java.util.ArrayList;
21 import java.util.Date;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Map;
26 
27 /**
28  * Simple http test server for testing.
29  *
30  * Extends WebServer with the ability to map requests to prepared responses.
31  */
32 public class TestWebServer extends WebServer {
33     private static final String TAG = "TestWebServer";
34 
35     private static class Response {
36         final byte[] mResponseData;
37         final List<Pair<String, String>> mResponseHeaders;
38         final boolean mIsRedirect;
39         final Runnable mResponseAction;
40         final boolean mIsNotFound;
41         final boolean mIsNoContent;
42         final boolean mForWebSocket;
43         final boolean mIsEmptyResponse;
44 
Response(byte[] responseData, List<Pair<String, String>> responseHeaders, boolean isRedirect, boolean isNotFound, boolean isNoContent, boolean forWebSocket, boolean isEmptyResponse, Runnable responseAction)45         Response(byte[] responseData, List<Pair<String, String>> responseHeaders,
46                 boolean isRedirect, boolean isNotFound, boolean isNoContent, boolean forWebSocket,
47                 boolean isEmptyResponse, Runnable responseAction) {
48             mIsRedirect = isRedirect;
49             mIsNotFound = isNotFound;
50             mIsNoContent = isNoContent;
51             mForWebSocket = forWebSocket;
52             mIsEmptyResponse = isEmptyResponse;
53             mResponseData = responseData;
54             mResponseHeaders = responseHeaders == null ? new ArrayList<Pair<String, String>>()
55                                                        : responseHeaders;
56             mResponseAction = responseAction;
57         }
58     }
59 
60     // The Maps below are modified on both the client thread and the internal server thread, so
61     // need to use a lock when accessing them.
62     private final Object mLock = new Object();
63     private final Map<String, Response> mResponseMap = new HashMap<String, Response>();
64     private final Map<String, Integer> mResponseCountMap = new HashMap<String, Integer>();
65     private final Map<String, HTTPRequest> mLastRequestMap = new HashMap<String, HTTPRequest>();
66 
67     /**
68      * Create and start a local HTTP server instance.
69      * @param port Port number the server must use, or 0 to automatically choose a free port.
70      * @param ssl True if the server should be using secure sockets.
71      * @param additional True if creating an additional server instance.
72      * @throws Exception
73      */
TestWebServer(int port, boolean ssl, boolean additional)74     private TestWebServer(int port, boolean ssl, boolean additional) throws Exception {
75         super(port, ssl, additional);
76         setRequestHandler(new Handler());
77     }
78 
79     private class Handler implements WebServer.RequestHandler {
80         @Override
handleRequest(WebServer.HTTPRequest request, OutputStream stream)81         public void handleRequest(WebServer.HTTPRequest request, OutputStream stream) {
82             WebServerPrintStream printStream = new WebServerPrintStream(stream);
83             try {
84                 outputResponse(request, printStream);
85             } catch (NoSuchAlgorithmException ignore) {
86             } catch (IOException e) {
87                 Log.w(TAG, e);
88             } finally {
89                 printStream.close();
90             }
91         }
92     }
93 
94     /**
95      * Create and start a local HTTP server instance. This function must only
96      * be called if no other instances were created. You are responsible for
97      * calling shutdown() on each instance you create.
98      *
99      * @param port Port number the server must use, or 0 to automatically choose a free port.
100      */
start(int port)101     public static TestWebServer start(int port) throws Exception {
102         return new TestWebServer(port, false, false);
103     }
104 
105     /**
106      * Same as start(int) but chooses a free port.
107      */
start()108     public static TestWebServer start() throws Exception {
109         return start(0);
110     }
111 
112     /**
113      * Create and start a local HTTP server instance. This function must only
114      * be called if you need more than one server instance and the first one
115      * was already created using start() or start(int). You are responsible for
116      * calling shutdown() on each instance you create.
117      *
118      * @param port Port number the server must use, or 0 to automatically choose a free port.
119      */
startAdditional(int port)120     public static TestWebServer startAdditional(int port) throws Exception {
121         return new TestWebServer(port, false, true);
122     }
123 
124     /**
125      * Same as startAdditional(int) but chooses a free port.
126      */
startAdditional()127     public static TestWebServer startAdditional() throws Exception {
128         return startAdditional(0);
129     }
130 
131     /**
132      * Create and start a local secure HTTP server instance. This function must
133      * only be called if no other secure instances were created. You are
134      * responsible for calling shutdown() on each instance you create.
135      *
136      * @param port Port number the server must use, or 0 to automatically choose a free port.
137      */
startSsl(int port)138     public static TestWebServer startSsl(int port) throws Exception {
139         return new TestWebServer(port, true, false);
140     }
141 
142     /**
143      * Same as startSsl(int) but chooses a free port.
144      */
startSsl()145     public static TestWebServer startSsl() throws Exception {
146         return startSsl(0);
147     }
148 
149     /**
150      * Create and start a local secure HTTP server instance. This function must
151      * only be called if you need more than one secure server instance and the
152      * first one was already created using startSsl() or startSsl(int). You are
153      * responsible for calling shutdown() on each instance you create.
154      *
155      * @param port Port number the server must use, or 0 to automatically choose a free port.
156      */
startAdditionalSsl(int port)157     public static TestWebServer startAdditionalSsl(int port) throws Exception {
158         return new TestWebServer(port, true, true);
159     }
160 
161     /**
162      * Same as startAdditionalSsl(int) but chooses a free port.
163      */
startAdditionalSsl()164     public static TestWebServer startAdditionalSsl() throws Exception {
165         return startAdditionalSsl(0);
166     }
167 
168     private static final int RESPONSE_STATUS_NORMAL = 0;
169     private static final int RESPONSE_STATUS_MOVED_TEMPORARILY = 1;
170     private static final int RESPONSE_STATUS_NOT_FOUND = 2;
171     private static final int RESPONSE_STATUS_NO_CONTENT = 3;
172     private static final int RESPONSE_STATUS_FOR_WEBSOCKET = 4;
173     private static final int RESPONSE_STATUS_EMPTY_RESPONSE = 5;
174 
setResponseInternal(String requestPath, byte[] responseData, List<Pair<String, String>> responseHeaders, Runnable responseAction, int status)175     private String setResponseInternal(String requestPath, byte[] responseData,
176             List<Pair<String, String>> responseHeaders, Runnable responseAction, int status) {
177         final boolean isRedirect = (status == RESPONSE_STATUS_MOVED_TEMPORARILY);
178         final boolean isNotFound = (status == RESPONSE_STATUS_NOT_FOUND);
179         final boolean isNoContent = (status == RESPONSE_STATUS_NO_CONTENT);
180         final boolean forWebSocket = (status == RESPONSE_STATUS_FOR_WEBSOCKET);
181         final boolean isEmptyResponse = (status == RESPONSE_STATUS_EMPTY_RESPONSE);
182 
183         synchronized (mLock) {
184             mResponseMap.put(requestPath,
185                     new Response(responseData, responseHeaders, isRedirect, isNotFound, isNoContent,
186                             forWebSocket, isEmptyResponse, responseAction));
187             mResponseCountMap.put(requestPath, Integer.valueOf(0));
188             mLastRequestMap.put(requestPath, null);
189         }
190         return getResponseUrl(requestPath);
191     }
192 
193     /**
194      * Sets a 404 (not found) response to be returned when a particular request path is passed in.
195      *
196      * @param requestPath The path to respond to.
197      * @return The full URL including the path that should be requested to get the expected
198      *         response.
199      */
setResponseWithNotFoundStatus(String requestPath)200     public String setResponseWithNotFoundStatus(String requestPath) {
201         return setResponseWithNotFoundStatus(requestPath, null);
202     }
203 
204     /**
205      * Sets a 404 (not found) response to be returned when a particular request path is passed in.
206      *
207      * @param requestPath The path to respond to.
208      * @param responseHeaders Any additional headers that should be returned along with the
209      *                        response (null is acceptable).
210      * @return The full URL including the path that should be requested to get the expected
211      *         response.
212      */
setResponseWithNotFoundStatus( String requestPath, List<Pair<String, String>> responseHeaders)213     public String setResponseWithNotFoundStatus(
214             String requestPath, List<Pair<String, String>> responseHeaders) {
215         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(""),
216                 responseHeaders, null, RESPONSE_STATUS_NOT_FOUND);
217     }
218 
219     /**
220      * Sets a 204 (no content) response to be returned when a particular request path is passed in.
221      *
222      * @param requestPath The path to respond to.
223      * @return The full URL including the path that should be requested to get the expected
224      *         response.
225      */
setResponseWithNoContentStatus(String requestPath)226     public String setResponseWithNoContentStatus(String requestPath) {
227         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(""), null, null,
228                 RESPONSE_STATUS_NO_CONTENT);
229     }
230 
231     /**
232      * Sets an empty response to be returned when a particular request path is passed in.
233      *
234      * @param requestPath The path to respond to.
235      * @return The full URL including the path that should be requested to get the expected
236      *         response.
237      */
setEmptyResponse(String requestPath)238     public String setEmptyResponse(String requestPath) {
239         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(""), null, null,
240                 RESPONSE_STATUS_EMPTY_RESPONSE);
241     }
242 
243     /**
244      * Sets a response to be returned when a particular request path is passed
245      * in (with the option to specify additional headers).
246      *
247      * @param requestPath The path to respond to.
248      * @param responseString The response body that will be returned.
249      * @param responseHeaders Any additional headers that should be returned along with the
250      *                        response (null is acceptable).
251      * @return The full URL including the path that should be requested to get the expected
252      *         response.
253      */
setResponse( String requestPath, String responseString, List<Pair<String, String>> responseHeaders)254     public String setResponse(
255             String requestPath, String responseString, List<Pair<String, String>> responseHeaders) {
256         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(responseString),
257                 responseHeaders, null, RESPONSE_STATUS_NORMAL);
258     }
259 
260     /**
261      * Sets a response to be returned when a particular request path is passed
262      * in with the option to specify additional headers as well as an arbitrary action to be
263      * executed on each request.
264      *
265      * @param requestPath The path to respond to.
266      * @param responseString The response body that will be returned.
267      * @param responseHeaders Any additional headers that should be returned along with the
268      *                        response (null is acceptable).
269      * @param responseAction The action to be performed when fetching the response.  This action
270      *                       will be executed for each request and will be handled on a background
271      *                       thread.
272      * @return The full URL including the path that should be requested to get the expected
273      *         response.
274      */
setResponseWithRunnableAction(String requestPath, String responseString, List<Pair<String, String>> responseHeaders, Runnable responseAction)275     public String setResponseWithRunnableAction(String requestPath, String responseString,
276             List<Pair<String, String>> responseHeaders, Runnable responseAction) {
277         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(responseString),
278                 responseHeaders, responseAction, RESPONSE_STATUS_NORMAL);
279     }
280 
281     /**
282      * Sets a redirect.
283      *
284      * @param requestPath The path to respond to.
285      * @param targetLocation The path (or absolute URL) to redirect to.
286      * @return The full URL including the path that should be requested to get the expected
287      *         response.
288      */
setRedirect(String requestPath, String targetLocation)289     public String setRedirect(String requestPath, String targetLocation) {
290         return setRedirect(requestPath, targetLocation, new ArrayList<>());
291     }
292 
293     /**
294      * Sets a redirect with optional headers.
295      *
296      * @param requestPath The path to respond to.
297      * @param targetLocation The path (or absolute URL) to redirect to.
298      * @param responseHeaders Any additional headers that should be returned along with the
299      *                        response (null is acceptable).
300      * @return The full URL including the path that should be requested to get the expected
301      *         response.
302      */
setRedirect( String requestPath, String targetLocation, List<Pair<String, String>> responseHeaders)303     public String setRedirect(
304             String requestPath, String targetLocation, List<Pair<String, String>> responseHeaders) {
305         responseHeaders.add(Pair.create("Location", targetLocation));
306 
307         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(targetLocation),
308                 responseHeaders, null, RESPONSE_STATUS_MOVED_TEMPORARILY);
309     }
310 
311     /**
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).
314      *
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
321      *         response.
322      */
setResponseBase64(String requestPath, String base64EncodedResponse, List<Pair<String, String>> responseHeaders)323     public String setResponseBase64(String requestPath, String base64EncodedResponse,
324             List<Pair<String, String>> responseHeaders) {
325         return setResponseInternal(requestPath,
326                 Base64.decode(base64EncodedResponse, Base64.DEFAULT), responseHeaders, null,
327                 RESPONSE_STATUS_NORMAL);
328     }
329 
330     /**
331      * Sets a response to a WebSocket handshake request.
332      *
333      * @param requestPath The path to respond to.
334      * @param responseHeaders Any additional headers that should be returned along with the
335      *                        response (null is acceptable).
336      * @return The full URL including the path that should be requested to get the expected
337      *         response.
338      */
setResponseForWebSocket( String requestPath, List<Pair<String, String>> responseHeaders)339     public String setResponseForWebSocket(
340             String requestPath, List<Pair<String, String>> responseHeaders) {
341         if (responseHeaders == null) {
342             responseHeaders = new ArrayList<Pair<String, String>>();
343         } else {
344             responseHeaders = new ArrayList<Pair<String, String>>(responseHeaders);
345         }
346         responseHeaders.add(Pair.create("Connection", "Upgrade"));
347         responseHeaders.add(Pair.create("Upgrade", "websocket"));
348         return setResponseInternal(requestPath, ApiCompatibilityUtils.getBytesUtf8(""),
349                 responseHeaders, null, RESPONSE_STATUS_FOR_WEBSOCKET);
350     }
351 
352     /**
353      * Get the number of requests was made at this path since it was last set.
354      */
getRequestCount(String requestPath)355     public int getRequestCount(String requestPath) {
356         Integer count = null;
357         synchronized (mLock) {
358             count = mResponseCountMap.get(requestPath);
359         }
360         if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath);
361         return count.intValue();
362     }
363 
364     /**
365      * Returns the last HttpRequest at this path. Can return null if it is never requested.
366      */
getLastRequest(String requestPath)367     public HTTPRequest getLastRequest(String requestPath) {
368         synchronized (mLock) {
369             if (!mLastRequestMap.containsKey(requestPath)) {
370                 throw new IllegalArgumentException("Path not set: " + requestPath);
371             }
372             return mLastRequestMap.get(requestPath);
373         }
374     }
375 
376     private static class WebServerPrintStream extends PrintStream {
WebServerPrintStream(OutputStream out)377         WebServerPrintStream(OutputStream out) {
378             super(out);
379         }
380 
381         @Override
println(String s)382         public void println(String s) {
383             Log.w(TAG, s);
384             super.println(s);
385         }
386     }
387 
388     /**
389      * Generate a response to the given request.
390      *
391      * <p>Always executed on the background server thread.
392      *
393      * <p>If there is an action associated with the response, it will be executed inside of
394      * this function.
395      *
396      * @throws NoSuchAlgorithmException, IOException
397      */
outputResponse(HTTPRequest request, WebServerPrintStream stream)398     private void outputResponse(HTTPRequest request, WebServerPrintStream stream)
399             throws NoSuchAlgorithmException, IOException {
400         // Don't dump headers to decrease log.
401         Log.w(TAG, request.requestLine());
402 
403         final String bodyTemplate = "<html><head><title>%s</title></head>"
404                 + "<body>%s</body></html>";
405 
406         boolean copyHeadersToResponse = true;
407         boolean copyBinaryBodyToResponse = false;
408         boolean contentLengthAlreadyIncluded = false;
409         boolean contentTypeAlreadyIncluded = false;
410         StringBuilder textBody = new StringBuilder();
411 
412         String requestURI = request.getURI();
413 
414         Response response;
415         synchronized (mLock) {
416             response = mResponseMap.get(requestURI);
417         }
418 
419         if (response == null || response.mIsNotFound) {
420             stream.println("HTTP/1.0 404 Not Found");
421             textBody.append(String.format(bodyTemplate, "Not Found", "Not Found"));
422         } else if (response.mForWebSocket) {
423             String keyHeader = request.headerValue("Sec-WebSocket-Key");
424             if (!keyHeader.isEmpty()) {
425                 stream.println("HTTP/1.0 101 Switching Protocols");
426                 stream.println("Sec-WebSocket-Accept: " + computeWebSocketAccept(keyHeader));
427             } else {
428                 stream.println("HTTP/1.0 404 Not Found");
429                 textBody.append(String.format(bodyTemplate, "Not Found", "Not Found"));
430                 copyHeadersToResponse = false;
431             }
432         } else if (response.mIsNoContent) {
433             stream.println("HTTP/1.0 204 No Content");
434             copyHeadersToResponse = false;
435         } else if (response.mIsRedirect) {
436             stream.println("HTTP/1.0 302 Found");
437             textBody.append(String.format(bodyTemplate, "Found", "Found"));
438         } else if (response.mIsEmptyResponse) {
439             stream.println("HTTP/1.0 200 OK");
440             copyHeadersToResponse = false;
441         } else {
442             if (response.mResponseAction != null) response.mResponseAction.run();
443 
444             stream.println("HTTP/1.0 200 OK");
445             copyBinaryBodyToResponse = true;
446         }
447 
448         if (response != null) {
449             if (copyHeadersToResponse) {
450                 for (Pair<String, String> header : response.mResponseHeaders) {
451                     stream.println(header.first + ": " + header.second);
452                     if (header.first.toLowerCase(Locale.ENGLISH).equals("content-length")) {
453                         contentLengthAlreadyIncluded = true;
454                     } else if (header.first.toLowerCase(Locale.ENGLISH).equals("content-type")) {
455                         contentTypeAlreadyIncluded = true;
456                     }
457                 }
458             }
459             synchronized (mLock) {
460                 mResponseCountMap.put(requestURI,
461                         Integer.valueOf(mResponseCountMap.get(requestURI).intValue() + 1));
462                 mLastRequestMap.put(requestURI, request);
463             }
464         }
465 
466         // RFC 1123
467         final SimpleDateFormat dateFormat =
468                 new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
469 
470         // Using print and println() because we don't want to dump it into log.
471         stream.print("Date: " + dateFormat.format(new Date()));
472         stream.println();
473 
474         if (textBody.length() != 0) {
475             if (!contentTypeAlreadyIncluded
476                     && (requestURI.endsWith(".html") || requestURI.endsWith(".htm"))) {
477                 stream.println("Content-Type: text/html");
478             }
479             stream.println("Content-Length: " + textBody.length());
480             stream.println();
481             stream.print(textBody.toString());
482         } else if (copyBinaryBodyToResponse) {
483             if (!contentTypeAlreadyIncluded && requestURI.endsWith(".js")) {
484                 stream.println("Content-Type: application/javascript");
485             } else if (!contentTypeAlreadyIncluded
486                     && (requestURI.endsWith(".html") || requestURI.endsWith(".htm"))) {
487                 stream.println("Content-Type: text/html");
488             }
489             if (!contentLengthAlreadyIncluded) {
490                 stream.println("Content-Length: " + response.mResponseData.length);
491             }
492             stream.println();
493             stream.write(response.mResponseData);
494         } else {
495             stream.println();
496         }
497     }
498 
499     /**
500      * Return a response for WebSocket handshake challenge.
501      */
computeWebSocketAccept(String keyString)502     private static String computeWebSocketAccept(String keyString) throws NoSuchAlgorithmException {
503         byte[] key = keyString.getBytes(Charset.forName("US-ASCII"));
504         byte[] guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(Charset.forName("US-ASCII"));
505 
506         MessageDigest md = MessageDigest.getInstance("SHA");
507         md.update(key);
508         md.update(guid);
509         byte[] output = md.digest();
510         return Base64.encodeToString(output, Base64.NO_WRAP);
511     }
512 }
513