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