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