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