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