TestWebServer.java revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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
5package org.chromium.net.test.util;
6
7import android.util.Base64;
8import android.util.Log;
9import android.util.Pair;
10
11import org.apache.http.HttpException;
12import org.apache.http.HttpRequest;
13import org.apache.http.HttpResponse;
14import org.apache.http.HttpStatus;
15import org.apache.http.HttpVersion;
16import org.apache.http.RequestLine;
17import org.apache.http.StatusLine;
18import org.apache.http.entity.ByteArrayEntity;
19import org.apache.http.impl.DefaultHttpServerConnection;
20import org.apache.http.impl.cookie.DateUtils;
21import org.apache.http.message.BasicHttpResponse;
22import org.apache.http.params.BasicHttpParams;
23import org.apache.http.params.CoreProtocolPNames;
24import org.apache.http.params.HttpParams;
25
26import java.io.ByteArrayInputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.net.MalformedURLException;
30import java.net.ServerSocket;
31import java.net.Socket;
32import java.net.URI;
33import java.net.URL;
34import java.net.URLConnection;
35import java.security.KeyManagementException;
36import java.security.KeyStore;
37import java.security.NoSuchAlgorithmException;
38import java.security.cert.X509Certificate;
39import java.util.ArrayList;
40import java.util.Date;
41import java.util.HashMap;
42import java.util.Hashtable;
43import java.util.List;
44import java.util.Map;
45
46import javax.net.ssl.HostnameVerifier;
47import javax.net.ssl.HttpsURLConnection;
48import javax.net.ssl.KeyManager;
49import javax.net.ssl.KeyManagerFactory;
50import javax.net.ssl.SSLContext;
51import javax.net.ssl.SSLSession;
52import 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 */
63public 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
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     */
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     */
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
315    public String getBaseUrl() {
316        return mServerUri + "/";
317    }
318
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
348        public void checkClientTrusted(X509Certificate[] chain, String authType) {
349            // Trust the TestWebServer...
350        }
351
352        @Override
353        public void checkServerTrusted(X509Certificate[] chain, String authType) {
354            // Trust the TestWebServer...
355        }
356
357        @Override
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
370        public boolean verify(String hostname, SSLSession session) {
371            return true;
372        }
373    }
374
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     */
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
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     */
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     */
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         */
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
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
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
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