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