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