1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.webkit.cts; 17 18 import android.content.Context; 19 import android.content.res.AssetManager; 20 import android.content.res.Resources; 21 import android.net.Uri; 22 import android.os.Environment; 23 import android.util.Base64; 24 import android.util.Log; 25 import android.webkit.MimeTypeMap; 26 27 import org.apache.http.Header; 28 import org.apache.http.HttpEntity; 29 import org.apache.http.HttpEntityEnclosingRequest; 30 import org.apache.http.HttpException; 31 import org.apache.http.HttpRequest; 32 import org.apache.http.HttpResponse; 33 import org.apache.http.HttpStatus; 34 import org.apache.http.HttpVersion; 35 import org.apache.http.NameValuePair; 36 import org.apache.http.RequestLine; 37 import org.apache.http.StatusLine; 38 import org.apache.http.client.utils.URLEncodedUtils; 39 import org.apache.http.entity.ByteArrayEntity; 40 import org.apache.http.entity.FileEntity; 41 import org.apache.http.entity.InputStreamEntity; 42 import org.apache.http.entity.StringEntity; 43 import org.apache.http.impl.DefaultHttpServerConnection; 44 import org.apache.http.impl.cookie.DateUtils; 45 import org.apache.http.message.BasicHttpResponse; 46 import org.apache.http.params.BasicHttpParams; 47 import org.apache.http.params.CoreProtocolPNames; 48 import org.apache.http.params.HttpParams; 49 50 import java.io.BufferedOutputStream; 51 import java.io.ByteArrayInputStream; 52 import java.io.ByteArrayOutputStream; 53 import java.io.File; 54 import java.io.FileInputStream; 55 import java.io.FileOutputStream; 56 import java.io.IOException; 57 import java.io.InputStream; 58 import java.io.UnsupportedEncodingException; 59 import java.net.ServerSocket; 60 import java.net.Socket; 61 import java.net.URI; 62 import java.net.URLEncoder; 63 import java.security.Key; 64 import java.security.KeyFactory; 65 import java.security.KeyStore; 66 import java.security.cert.Certificate; 67 import java.security.cert.CertificateFactory; 68 import java.security.cert.X509Certificate; 69 import java.security.spec.PKCS8EncodedKeySpec; 70 import java.util.ArrayList; 71 import java.util.Date; 72 import java.util.HashMap; 73 import java.util.HashSet; 74 import java.util.Hashtable; 75 import java.util.Iterator; 76 import java.util.List; 77 import java.util.Map; 78 import java.util.Set; 79 import java.util.Vector; 80 import java.util.concurrent.ExecutorService; 81 import java.util.concurrent.Executors; 82 import java.util.concurrent.RejectedExecutionException; 83 import java.util.concurrent.TimeUnit; 84 import java.util.regex.Matcher; 85 import java.util.regex.Pattern; 86 87 import javax.net.ssl.HostnameVerifier; 88 import javax.net.ssl.HttpsURLConnection; 89 import javax.net.ssl.KeyManager; 90 import javax.net.ssl.KeyManagerFactory; 91 import javax.net.ssl.SSLContext; 92 import javax.net.ssl.SSLServerSocket; 93 import javax.net.ssl.SSLSession; 94 import javax.net.ssl.X509TrustManager; 95 96 /** 97 * Simple http test server for testing webkit client functionality. 98 */ 99 public class CtsTestServer { 100 private static final String TAG = "CtsTestServer"; 101 102 public static final String FAVICON_PATH = "/favicon.ico"; 103 public static final String USERAGENT_PATH = "/useragent.html"; 104 105 public static final String TEST_DOWNLOAD_PATH = "/download.html"; 106 private static final String DOWNLOAD_ID_PARAMETER = "downloadId"; 107 private static final String NUM_BYTES_PARAMETER = "numBytes"; 108 109 private static final String ASSET_PREFIX = "/assets/"; 110 private static final String RAW_PREFIX = "raw/"; 111 private static final String FAVICON_ASSET_PATH = ASSET_PREFIX + "webkit/favicon.png"; 112 private static final String APPCACHE_PATH = "/appcache.html"; 113 private static final String APPCACHE_MANIFEST_PATH = "/appcache.manifest"; 114 private static final String REDIRECT_PREFIX = "/redirect"; 115 private static final String QUERY_REDIRECT_PATH = "/alt_redirect"; 116 private static final String DELAY_PREFIX = "/delayed"; 117 private static final String BINARY_PREFIX = "/binary"; 118 private static final String SET_COOKIE_PREFIX = "/setcookie"; 119 private static final String COOKIE_PREFIX = "/cookie"; 120 private static final String LINKED_SCRIPT_PREFIX = "/linkedscriptprefix"; 121 private static final String AUTH_PREFIX = "/auth"; 122 public static final String NOLENGTH_POSTFIX = "nolength"; 123 private static final int DELAY_MILLIS = 2000; 124 125 public static final String AUTH_REALM = "Android CTS"; 126 public static final String AUTH_USER = "cts"; 127 public static final String AUTH_PASS = "secret"; 128 // base64 encoded credentials "cts:secret" used for basic authentication 129 public static final String AUTH_CREDENTIALS = "Basic Y3RzOnNlY3JldA=="; 130 131 public static final String MESSAGE_401 = "401 unauthorized"; 132 public static final String MESSAGE_403 = "403 forbidden"; 133 public static final String MESSAGE_404 = "404 not found"; 134 135 public enum SslMode { 136 INSECURE, 137 NO_CLIENT_AUTH, 138 WANTS_CLIENT_AUTH, 139 NEEDS_CLIENT_AUTH, 140 TRUST_ANY_CLIENT 141 } 142 143 private static Hashtable<Integer, String> sReasons; 144 145 private ServerThread mServerThread; 146 private String mServerUri; 147 private AssetManager mAssets; 148 private Context mContext; 149 private Resources mResources; 150 private SslMode mSsl; 151 private MimeTypeMap mMap; 152 private Vector<String> mQueries; 153 private ArrayList<HttpEntity> mRequestEntities; 154 private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>(); 155 private long mDocValidity; 156 private long mDocAge; 157 private X509TrustManager mTrustManager; 158 159 /** 160 * Create and start a local HTTP server instance. 161 * @param context The application context to use for fetching assets. 162 * @throws IOException 163 */ CtsTestServer(Context context)164 public CtsTestServer(Context context) throws Exception { 165 this(context, false); 166 } 167 getReasonString(int status)168 public static String getReasonString(int status) { 169 if (sReasons == null) { 170 sReasons = new Hashtable<Integer, String>(); 171 sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized"); 172 sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found"); 173 sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden"); 174 sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily"); 175 } 176 return sReasons.get(status); 177 } 178 179 /** 180 * Create and start a local HTTP server instance. 181 * @param context The application context to use for fetching assets. 182 * @param ssl True if the server should be using secure sockets. 183 * @throws Exception 184 */ CtsTestServer(Context context, boolean ssl)185 public CtsTestServer(Context context, boolean ssl) throws Exception { 186 this(context, ssl ? SslMode.NO_CLIENT_AUTH : SslMode.INSECURE); 187 } 188 189 /** 190 * Create and start a local HTTP server instance. 191 * @param context The application context to use for fetching assets. 192 * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. 193 * @throws Exception 194 */ CtsTestServer(Context context, SslMode sslMode)195 public CtsTestServer(Context context, SslMode sslMode) throws Exception { 196 this(context, sslMode, 0, 0); 197 } 198 199 /** 200 * Create and start a local HTTP server instance. 201 * @param context The application context to use for fetching assets. 202 * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. 203 * @param trustManager the trustManager 204 * @throws Exception 205 */ CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager)206 public CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager) 207 throws Exception { 208 this(context, sslMode, trustManager, 0, 0); 209 } 210 211 /** 212 * Create and start a local HTTP server instance. 213 * @param context The application context to use for fetching assets. 214 * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. 215 * @param keyResId Raw resource ID of the server private key to use. 216 * @param certResId Raw resource ID of the server certificate to use. 217 * @throws Exception 218 */ CtsTestServer(Context context, SslMode sslMode, int keyResId, int certResId)219 public CtsTestServer(Context context, SslMode sslMode, int keyResId, int certResId) 220 throws Exception { 221 this(context, sslMode, new CtsTrustManager(), keyResId, certResId); 222 } 223 224 /** 225 * Create and start a local HTTP server instance. 226 * @param context The application context to use for fetching assets. 227 * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. 228 * @param trustManager the trustManager 229 * @param keyResId Raw resource ID of the server private key to use. 230 * @param certResId Raw resource ID of the server certificate to use. 231 * @throws Exception 232 */ CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager, int keyResId, int certResId)233 public CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager, 234 int keyResId, int certResId) throws Exception { 235 mContext = context; 236 mAssets = mContext.getAssets(); 237 mResources = mContext.getResources(); 238 mSsl = sslMode; 239 mRequestEntities = new ArrayList<HttpEntity>(); 240 mMap = MimeTypeMap.getSingleton(); 241 mQueries = new Vector<String>(); 242 mTrustManager = trustManager; 243 if (keyResId == 0 && certResId == 0) { 244 mServerThread = new ServerThread(this, mSsl, null, null); 245 } else { 246 mServerThread = new ServerThread(this, mSsl, mResources.openRawResource(keyResId), 247 mResources.openRawResource(certResId)); 248 } 249 if (mSsl == SslMode.INSECURE) { 250 mServerUri = "http:"; 251 } else { 252 mServerUri = "https:"; 253 } 254 mServerUri += "//localhost:" + mServerThread.mSocket.getLocalPort(); 255 mServerThread.start(); 256 } 257 258 /** 259 * Terminate the http server. 260 */ shutdown()261 public void shutdown() { 262 mServerThread.shutDownOnClientThread(); 263 264 try { 265 // Block until the server thread is done shutting down. 266 mServerThread.join(); 267 } catch (InterruptedException e) { 268 throw new RuntimeException(e); 269 } 270 } 271 272 /** 273 * {@link X509TrustManager} that trusts everybody. This is used so that 274 * the client calling {@link CtsTestServer#shutdown()} can issue a request 275 * for shutdown by blindly trusting the {@link CtsTestServer}'s 276 * credentials. 277 */ 278 private static class CtsTrustManager implements X509TrustManager { checkClientTrusted(X509Certificate[] chain, String authType)279 public void checkClientTrusted(X509Certificate[] chain, String authType) { 280 // Trust the CtSTestServer's client... 281 } 282 checkServerTrusted(X509Certificate[] chain, String authType)283 public void checkServerTrusted(X509Certificate[] chain, String authType) { 284 // Trust the CtSTestServer... 285 } 286 getAcceptedIssuers()287 public X509Certificate[] getAcceptedIssuers() { 288 return null; 289 } 290 } 291 292 /** 293 * @return a trust manager array of size 1. 294 */ getTrustManagers()295 private X509TrustManager[] getTrustManagers() { 296 return new X509TrustManager[] { mTrustManager }; 297 } 298 299 /** 300 * {@link HostnameVerifier} that verifies everybody. This permits 301 * the client to trust the web server and call 302 * {@link CtsTestServer#shutdown()}. 303 */ 304 private static class CtsHostnameVerifier implements HostnameVerifier { verify(String hostname, SSLSession session)305 public boolean verify(String hostname, SSLSession session) { 306 return true; 307 } 308 } 309 310 /** 311 * Return the URI that points to the server root. 312 */ getBaseUri()313 public String getBaseUri() { 314 return mServerUri; 315 } 316 317 /** 318 * Return the absolute URL that refers to the given asset. 319 * @param path The path of the asset. See {@link AssetManager#open(String)} 320 */ getAssetUrl(String path)321 public String getAssetUrl(String path) { 322 StringBuilder sb = new StringBuilder(getBaseUri()); 323 sb.append(ASSET_PREFIX); 324 sb.append(path); 325 return sb.toString(); 326 } 327 328 /** 329 * Return an artificially delayed absolute URL that refers to the given asset. This can be 330 * used to emulate a slow HTTP server or connection. 331 * @param path The path of the asset. See {@link AssetManager#open(String)} 332 */ getDelayedAssetUrl(String path)333 public String getDelayedAssetUrl(String path) { 334 return getDelayedAssetUrl(path, DELAY_MILLIS); 335 } 336 337 /** 338 * Return an artificially delayed absolute URL that refers to the given asset. This can be 339 * used to emulate a slow HTTP server or connection. 340 * @param path The path of the asset. See {@link AssetManager#open(String)} 341 * @param delayMs The number of milliseconds to delay the request 342 */ getDelayedAssetUrl(String path, int delayMs)343 public String getDelayedAssetUrl(String path, int delayMs) { 344 StringBuilder sb = new StringBuilder(getBaseUri()); 345 sb.append(DELAY_PREFIX); 346 sb.append("/"); 347 sb.append(delayMs); 348 sb.append(ASSET_PREFIX); 349 sb.append(path); 350 return sb.toString(); 351 } 352 353 /** 354 * Return an absolute URL that refers to the given asset and is protected by 355 * HTTP authentication. 356 * @param path The path of the asset. See {@link AssetManager#open(String)} 357 */ getAuthAssetUrl(String path)358 public String getAuthAssetUrl(String path) { 359 StringBuilder sb = new StringBuilder(getBaseUri()); 360 sb.append(AUTH_PREFIX); 361 sb.append(ASSET_PREFIX); 362 sb.append(path); 363 return sb.toString(); 364 } 365 366 /** 367 * Return an absolute URL that indirectly refers to the given asset. 368 * When a client fetches this URL, the server will respond with a temporary redirect (302) 369 * referring to the absolute URL of the given asset. 370 * @param path The path of the asset. See {@link AssetManager#open(String)} 371 */ getRedirectingAssetUrl(String path)372 public String getRedirectingAssetUrl(String path) { 373 return getRedirectingAssetUrl(path, 1); 374 } 375 376 /** 377 * Return an absolute URL that indirectly refers to the given asset. 378 * When a client fetches this URL, the server will respond with a temporary redirect (302) 379 * referring to the absolute URL of the given asset. 380 * @param path The path of the asset. See {@link AssetManager#open(String)} 381 * @param numRedirects The number of redirects required to reach the given asset. 382 */ getRedirectingAssetUrl(String path, int numRedirects)383 public String getRedirectingAssetUrl(String path, int numRedirects) { 384 StringBuilder sb = new StringBuilder(getBaseUri()); 385 for (int i = 0; i < numRedirects; i++) { 386 sb.append(REDIRECT_PREFIX); 387 } 388 sb.append(ASSET_PREFIX); 389 sb.append(path); 390 return sb.toString(); 391 } 392 393 /** 394 * Return an absolute URL that indirectly refers to the given asset, without having 395 * the destination path be part of the redirecting path. 396 * When a client fetches this URL, the server will respond with a temporary redirect (302) 397 * referring to the absolute URL of the given asset. 398 * @param path The path of the asset. See {@link AssetManager#open(String)} 399 */ getQueryRedirectingAssetUrl(String path)400 public String getQueryRedirectingAssetUrl(String path) { 401 StringBuilder sb = new StringBuilder(getBaseUri()); 402 sb.append(QUERY_REDIRECT_PATH); 403 sb.append("?dest="); 404 try { 405 sb.append(URLEncoder.encode(getAssetUrl(path), "UTF-8")); 406 } catch (UnsupportedEncodingException e) { 407 } 408 return sb.toString(); 409 } 410 411 /** 412 * getSetCookieUrl returns a URL that attempts to set the cookie 413 * "key=value" when fetched. 414 * @param path a suffix to disambiguate multiple Cookie URLs. 415 * @param key the key of the cookie. 416 * @return the url for a page that attempts to set the cookie. 417 */ getSetCookieUrl(String path, String key, String value)418 public String getSetCookieUrl(String path, String key, String value) { 419 return getSetCookieUrl(path, key, value, null); 420 } 421 422 /** 423 * getSetCookieUrl returns a URL that attempts to set the cookie 424 * "key=value" with the given list of attributes when fetched. 425 * @param path a suffix to disambiguate multiple Cookie URLs. 426 * @param key the key of the cookie 427 * @param attributes the attributes to set 428 * @return the url for a page that attempts to set the cookie. 429 */ getSetCookieUrl(String path, String key, String value, String attributes)430 public String getSetCookieUrl(String path, String key, String value, String attributes) { 431 StringBuilder sb = new StringBuilder(getBaseUri()); 432 sb.append(SET_COOKIE_PREFIX); 433 sb.append(path); 434 sb.append("?key="); 435 sb.append(key); 436 sb.append("&value="); 437 sb.append(value); 438 if (attributes != null) { 439 sb.append("&attributes="); 440 sb.append(attributes); 441 } 442 return sb.toString(); 443 } 444 445 /** 446 * getLinkedScriptUrl returns a URL for a page with a script tag where 447 * src equals the URL passed in. 448 * @param path a suffix to disambiguate mulitple Linked Script URLs. 449 * @param url the src of the script tag. 450 * @return the url for the page with the script link in. 451 */ getLinkedScriptUrl(String path, String url)452 public String getLinkedScriptUrl(String path, String url) { 453 StringBuilder sb = new StringBuilder(getBaseUri()); 454 sb.append(LINKED_SCRIPT_PREFIX); 455 sb.append(path); 456 sb.append("?url="); 457 try { 458 sb.append(URLEncoder.encode(url, "UTF-8")); 459 } catch (UnsupportedEncodingException e) { 460 } 461 return sb.toString(); 462 } 463 getBinaryUrl(String mimeType, int contentLength)464 public String getBinaryUrl(String mimeType, int contentLength) { 465 StringBuilder sb = new StringBuilder(getBaseUri()); 466 sb.append(BINARY_PREFIX); 467 sb.append("?type="); 468 sb.append(mimeType); 469 sb.append("&length="); 470 sb.append(contentLength); 471 return sb.toString(); 472 } 473 getCookieUrl(String path)474 public String getCookieUrl(String path) { 475 StringBuilder sb = new StringBuilder(getBaseUri()); 476 sb.append(COOKIE_PREFIX); 477 sb.append("/"); 478 sb.append(path); 479 return sb.toString(); 480 } 481 getUserAgentUrl()482 public String getUserAgentUrl() { 483 StringBuilder sb = new StringBuilder(getBaseUri()); 484 sb.append(USERAGENT_PATH); 485 return sb.toString(); 486 } 487 getAppCacheUrl()488 public String getAppCacheUrl() { 489 StringBuilder sb = new StringBuilder(getBaseUri()); 490 sb.append(APPCACHE_PATH); 491 return sb.toString(); 492 } 493 494 /** 495 * @param downloadId used to differentiate the files created for each test 496 * @param numBytes of the content that the CTS server should send back 497 * @return url to get the file from 498 */ getTestDownloadUrl(String downloadId, int numBytes)499 public String getTestDownloadUrl(String downloadId, int numBytes) { 500 return Uri.parse(getBaseUri()) 501 .buildUpon() 502 .path(TEST_DOWNLOAD_PATH) 503 .appendQueryParameter(DOWNLOAD_ID_PARAMETER, downloadId) 504 .appendQueryParameter(NUM_BYTES_PARAMETER, Integer.toString(numBytes)) 505 .build() 506 .toString(); 507 } 508 509 /** 510 * Returns true if the resource identified by url has been requested since 511 * the server was started or the last call to resetRequestState(). 512 * 513 * @param url The relative url to check whether it has been requested. 514 */ wasResourceRequested(String url)515 public synchronized boolean wasResourceRequested(String url) { 516 Iterator<String> it = mQueries.iterator(); 517 while (it.hasNext()) { 518 String request = it.next(); 519 if (request.endsWith(url)) { 520 return true; 521 } 522 } 523 return false; 524 } 525 526 /** 527 * Returns all received request entities since the last reset. 528 */ getRequestEntities()529 public synchronized ArrayList<HttpEntity> getRequestEntities() { 530 return mRequestEntities; 531 } 532 getRequestCount()533 public synchronized int getRequestCount() { 534 return mQueries.size(); 535 } 536 537 /** 538 * Set the validity of any future responses in milliseconds. If this is set to a non-zero 539 * value, the server will include a "Expires" header. 540 * @param timeMillis The time, in milliseconds, for which any future response will be valid. 541 */ setDocumentValidity(long timeMillis)542 public synchronized void setDocumentValidity(long timeMillis) { 543 mDocValidity = timeMillis; 544 } 545 546 /** 547 * Set the age of documents served. If this is set to a non-zero value, the server will include 548 * a "Last-Modified" header calculated from the value. 549 * @param timeMillis The age, in milliseconds, of any document served in the future. 550 */ setDocumentAge(long timeMillis)551 public synchronized void setDocumentAge(long timeMillis) { 552 mDocAge = timeMillis; 553 } 554 555 /** 556 * Resets the saved requests and request counts. 557 */ resetRequestState()558 public synchronized void resetRequestState() { 559 560 mQueries.clear(); 561 mRequestEntities = new ArrayList<HttpEntity>(); 562 } 563 564 /** 565 * Returns the last HttpRequest at this path. Can return null if it is never requested. 566 */ getLastRequest(String requestPath)567 public synchronized HttpRequest getLastRequest(String requestPath) { 568 String relativeUrl = getRelativeUrl(requestPath); 569 if (!mLastRequestMap.containsKey(relativeUrl)) 570 return null; 571 return mLastRequestMap.get(relativeUrl); 572 } 573 /** 574 * Hook for adding stuffs for HTTP POST. Default implementation does nothing. 575 * @return null to use the default response mechanism of sending the requested uri as it is. 576 * Otherwise, the whole response should be handled inside onPost. 577 */ onPost(HttpRequest request)578 protected HttpResponse onPost(HttpRequest request) throws Exception { 579 return null; 580 } 581 582 /** 583 * Return the relative URL that refers to the given asset. 584 * @param path The path of the asset. See {@link AssetManager#open(String)} 585 */ getRelativeUrl(String path)586 private String getRelativeUrl(String path) { 587 StringBuilder sb = new StringBuilder(ASSET_PREFIX); 588 sb.append(path); 589 return sb.toString(); 590 } 591 592 /** 593 * Generate a response to the given request. 594 * @throws InterruptedException 595 * @throws IOException 596 */ getResponse(HttpRequest request)597 private HttpResponse getResponse(HttpRequest request) throws Exception { 598 RequestLine requestLine = request.getRequestLine(); 599 HttpResponse response = null; 600 String uriString = requestLine.getUri(); 601 Log.i(TAG, requestLine.getMethod() + ": " + uriString); 602 603 synchronized (this) { 604 mQueries.add(uriString); 605 mLastRequestMap.put(uriString, request); 606 if (request instanceof HttpEntityEnclosingRequest) { 607 mRequestEntities.add(((HttpEntityEnclosingRequest)request).getEntity()); 608 } 609 } 610 611 if (requestLine.getMethod().equals("POST")) { 612 HttpResponse responseOnPost = onPost(request); 613 if (responseOnPost != null) { 614 return responseOnPost; 615 } 616 } 617 618 URI uri = URI.create(uriString); 619 String path = uri.getPath(); 620 String query = uri.getQuery(); 621 if (path.equals(FAVICON_PATH)) { 622 path = FAVICON_ASSET_PATH; 623 } 624 if (path.startsWith(DELAY_PREFIX)) { 625 String delayPath = path.substring(DELAY_PREFIX.length() + 1); 626 String delay = delayPath.substring(0, delayPath.indexOf('/')); 627 path = delayPath.substring(delay.length()); 628 try { 629 Thread.sleep(Integer.valueOf(delay)); 630 } catch (InterruptedException ignored) { 631 // ignore 632 } 633 } 634 if (path.startsWith(AUTH_PREFIX)) { 635 // authentication required 636 Header[] auth = request.getHeaders("Authorization"); 637 if ((auth.length > 0 && auth[0].getValue().equals(AUTH_CREDENTIALS)) 638 // This is a hack to make sure that loads to this url's will always 639 // ask for authentication. This is what the test expects. 640 && !path.endsWith("embedded_image.html")) { 641 // fall through and serve content 642 path = path.substring(AUTH_PREFIX.length()); 643 } else { 644 // request authorization 645 response = createResponse(HttpStatus.SC_UNAUTHORIZED); 646 response.addHeader("WWW-Authenticate", "Basic realm=\"" + AUTH_REALM + "\""); 647 } 648 } 649 if (path.startsWith(BINARY_PREFIX)) { 650 List <NameValuePair> args = URLEncodedUtils.parse(uri, "UTF-8"); 651 int length = 0; 652 String mimeType = null; 653 try { 654 for (NameValuePair pair : args) { 655 String name = pair.getName(); 656 if (name.equals("type")) { 657 mimeType = pair.getValue(); 658 } else if (name.equals("length")) { 659 length = Integer.parseInt(pair.getValue()); 660 } 661 } 662 if (length > 0 && mimeType != null) { 663 ByteArrayEntity entity = new ByteArrayEntity(new byte[length]); 664 entity.setContentType(mimeType); 665 response = createResponse(HttpStatus.SC_OK); 666 response.setEntity(entity); 667 response.addHeader("Content-Disposition", "attachment; filename=test.bin"); 668 response.addHeader("Content-Type", mimeType); 669 response.addHeader("Content-Length", "" + length); 670 } else { 671 // fall through, return 404 at the end 672 } 673 } catch (Exception e) { 674 // fall through, return 404 at the end 675 Log.w(TAG, e); 676 } 677 } else if (path.startsWith(ASSET_PREFIX)) { 678 path = path.substring(ASSET_PREFIX.length()); 679 // request for an asset file 680 try { 681 InputStream in; 682 if (path.startsWith(RAW_PREFIX)) { 683 String resourceName = path.substring(RAW_PREFIX.length()); 684 int id = mResources.getIdentifier(resourceName, "raw", mContext.getPackageName()); 685 if (id == 0) { 686 Log.w(TAG, "Can't find raw resource " + resourceName); 687 throw new IOException(); 688 } 689 in = mResources.openRawResource(id); 690 } else if (path.startsWith( 691 Environment.getExternalStorageDirectory().getAbsolutePath())) { 692 in = new FileInputStream(path); 693 } else { 694 in = mAssets.open(path); 695 } 696 response = createResponse(HttpStatus.SC_OK); 697 InputStreamEntity entity = new InputStreamEntity(in, in.available()); 698 String mimeType = 699 mMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(path)); 700 if (mimeType == null) { 701 mimeType = "text/html"; 702 } 703 entity.setContentType(mimeType); 704 response.setEntity(entity); 705 if (query == null || !query.contains(NOLENGTH_POSTFIX)) { 706 response.setHeader("Content-Length", "" + entity.getContentLength()); 707 } 708 } catch (IOException e) { 709 response = null; 710 // fall through, return 404 at the end 711 } 712 } else if (path.startsWith(REDIRECT_PREFIX)) { 713 response = createResponse(HttpStatus.SC_MOVED_TEMPORARILY); 714 String location = getBaseUri() + path.substring(REDIRECT_PREFIX.length()); 715 Log.i(TAG, "Redirecting to: " + location); 716 response.addHeader("Location", location); 717 } else if (path.equals(QUERY_REDIRECT_PATH)) { 718 String location = Uri.parse(uriString).getQueryParameter("dest"); 719 if (location != null) { 720 Log.i(TAG, "Redirecting to: " + location); 721 response = createResponse(HttpStatus.SC_MOVED_TEMPORARILY); 722 response.addHeader("Location", location); 723 } 724 } else if (path.startsWith(COOKIE_PREFIX)) { 725 /* 726 * Return a page with a title containing a list of all incoming cookies, 727 * separated by '|' characters. If a numeric 'count' value is passed in a cookie, 728 * return a cookie with the value incremented by 1. Otherwise, return a cookie 729 * setting 'count' to 0. 730 */ 731 response = createResponse(HttpStatus.SC_OK); 732 Header[] cookies = request.getHeaders("Cookie"); 733 Pattern p = Pattern.compile("count=(\\d+)"); 734 StringBuilder cookieString = new StringBuilder(100); 735 cookieString.append(cookies.length); 736 int count = 0; 737 for (Header cookie : cookies) { 738 cookieString.append("|"); 739 String value = cookie.getValue(); 740 cookieString.append(value); 741 Matcher m = p.matcher(value); 742 if (m.find()) { 743 count = Integer.parseInt(m.group(1)) + 1; 744 } 745 } 746 747 response.addHeader("Set-Cookie", "count=" + count + "; path=" + COOKIE_PREFIX); 748 response.setEntity(createPage(cookieString.toString(), cookieString.toString())); 749 } else if (path.startsWith(SET_COOKIE_PREFIX)) { 750 response = createResponse(HttpStatus.SC_OK); 751 Uri parsedUri = Uri.parse(uriString); 752 String key = parsedUri.getQueryParameter("key"); 753 String value = parsedUri.getQueryParameter("value"); 754 String attributes = parsedUri.getQueryParameter("attributes"); 755 String cookie = key + "=" + value; 756 if (attributes != null) { 757 cookie = cookie + "; " + attributes; 758 } 759 response.addHeader("Set-Cookie", cookie); 760 response.setEntity(createPage(cookie, cookie)); 761 } else if (path.startsWith(LINKED_SCRIPT_PREFIX)) { 762 response = createResponse(HttpStatus.SC_OK); 763 String src = Uri.parse(uriString).getQueryParameter("url"); 764 String scriptTag = "<script src=\"" + src + "\"></script>"; 765 response.setEntity(createPage("LinkedScript", scriptTag)); 766 } else if (path.equals(USERAGENT_PATH)) { 767 response = createResponse(HttpStatus.SC_OK); 768 Header agentHeader = request.getFirstHeader("User-Agent"); 769 String agent = ""; 770 if (agentHeader != null) { 771 agent = agentHeader.getValue(); 772 } 773 response.setEntity(createPage(agent, agent)); 774 } else if (path.equals(TEST_DOWNLOAD_PATH)) { 775 response = createTestDownloadResponse(mContext, Uri.parse(uriString)); 776 } else if (path.equals(APPCACHE_PATH)) { 777 response = createResponse(HttpStatus.SC_OK); 778 response.setEntity(createEntity("<!DOCTYPE HTML>" + 779 "<html manifest=\"appcache.manifest\">" + 780 " <head>" + 781 " <title>Waiting</title>" + 782 " <script>" + 783 " function updateTitle(x) { document.title = x; }" + 784 " window.applicationCache.onnoupdate = " + 785 " function() { updateTitle(\"onnoupdate Callback\"); };" + 786 " window.applicationCache.oncached = " + 787 " function() { updateTitle(\"oncached Callback\"); };" + 788 " window.applicationCache.onupdateready = " + 789 " function() { updateTitle(\"onupdateready Callback\"); };" + 790 " window.applicationCache.onobsolete = " + 791 " function() { updateTitle(\"onobsolete Callback\"); };" + 792 " window.applicationCache.onerror = " + 793 " function() { updateTitle(\"onerror Callback\"); };" + 794 " </script>" + 795 " </head>" + 796 " <body onload=\"updateTitle('Loaded');\">AppCache test</body>" + 797 "</html>")); 798 } else if (path.equals(APPCACHE_MANIFEST_PATH)) { 799 response = createResponse(HttpStatus.SC_OK); 800 try { 801 StringEntity entity = new StringEntity("CACHE MANIFEST"); 802 // This entity property is not used when constructing the response, (See 803 // AbstractMessageWriter.write(), which is called by 804 // AbstractHttpServerConnection.sendResponseHeader()) so we have to set this header 805 // manually. 806 // TODO: Should we do this for all responses from this server? 807 entity.setContentType("text/cache-manifest"); 808 response.setEntity(entity); 809 response.setHeader("Content-Type", "text/cache-manifest"); 810 } catch (UnsupportedEncodingException e) { 811 Log.w(TAG, "Unexpected UnsupportedEncodingException"); 812 } 813 } 814 if (response == null) { 815 response = createResponse(HttpStatus.SC_NOT_FOUND); 816 } 817 StatusLine sl = response.getStatusLine(); 818 Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")"); 819 setDateHeaders(response); 820 return response; 821 } 822 setDateHeaders(HttpResponse response)823 private void setDateHeaders(HttpResponse response) { 824 long time = System.currentTimeMillis(); 825 synchronized (this) { 826 if (mDocValidity != 0) { 827 String expires = DateUtils.formatDate(new Date(time + mDocValidity), 828 DateUtils.PATTERN_RFC1123); 829 response.addHeader("Expires", expires); 830 } 831 if (mDocAge != 0) { 832 String modified = DateUtils.formatDate(new Date(time - mDocAge), 833 DateUtils.PATTERN_RFC1123); 834 response.addHeader("Last-Modified", modified); 835 } 836 } 837 response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123)); 838 } 839 840 /** 841 * Create an empty response with the given status. 842 */ createResponse(int status)843 private static HttpResponse createResponse(int status) { 844 HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null); 845 846 // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is Locale-dependent. 847 String reason = getReasonString(status); 848 if (reason != null) { 849 response.setEntity(createPage(reason, reason)); 850 } 851 return response; 852 } 853 854 /** 855 * Create a string entity for the given content. 856 */ createEntity(String content)857 private static StringEntity createEntity(String content) { 858 try { 859 StringEntity entity = new StringEntity(content); 860 entity.setContentType("text/html"); 861 return entity; 862 } catch (UnsupportedEncodingException e) { 863 Log.w(TAG, e); 864 } 865 return null; 866 } 867 868 /** 869 * Create a string entity for a bare bones html page with provided title and body. 870 */ createPage(String title, String bodyContent)871 private static StringEntity createPage(String title, String bodyContent) { 872 return createEntity("<html><head><title>" + title + "</title></head>" + 873 "<body>" + bodyContent + "</body></html>"); 874 } 875 createTestDownloadResponse(Context context, Uri uri)876 private static HttpResponse createTestDownloadResponse(Context context, Uri uri) 877 throws IOException { 878 String downloadId = uri.getQueryParameter(DOWNLOAD_ID_PARAMETER); 879 int numBytes = uri.getQueryParameter(NUM_BYTES_PARAMETER) != null 880 ? Integer.parseInt(uri.getQueryParameter(NUM_BYTES_PARAMETER)) 881 : 0; 882 HttpResponse response = createResponse(HttpStatus.SC_OK); 883 response.setHeader("Content-Length", Integer.toString(numBytes)); 884 response.setEntity(createFileEntity(context, downloadId, numBytes)); 885 return response; 886 } 887 createFileEntity(Context context, String downloadId, int numBytes)888 private static FileEntity createFileEntity(Context context, String downloadId, int numBytes) 889 throws IOException { 890 String storageState = Environment.getExternalStorageState(); 891 if (Environment.MEDIA_MOUNTED.equalsIgnoreCase(storageState)) { 892 File storageDir = context.getExternalFilesDir(null); 893 File file = new File(storageDir, downloadId + ".bin"); 894 BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file)); 895 byte data[] = new byte[1024]; 896 for (int i = 0; i < data.length; i++) { 897 data[i] = 1; 898 } 899 try { 900 for (int i = 0; i < numBytes / data.length; i++) { 901 stream.write(data); 902 } 903 stream.write(data, 0, numBytes % data.length); 904 stream.flush(); 905 } finally { 906 stream.close(); 907 } 908 return new FileEntity(file, "application/octet-stream"); 909 } else { 910 throw new IllegalStateException("External storage must be mounted for this test!"); 911 } 912 } 913 createHttpServerConnection()914 protected DefaultHttpServerConnection createHttpServerConnection() { 915 return new DefaultHttpServerConnection(); 916 } 917 918 private static class ServerThread extends Thread { 919 private CtsTestServer mServer; 920 private ServerSocket mSocket; 921 private SslMode mSsl; 922 private boolean mWillShutDown = false; 923 private SSLContext mSslContext; 924 private ExecutorService mExecutorService = Executors.newFixedThreadPool(20); 925 private Object mLock = new Object(); 926 // All the sockets bound to an open connection. 927 private Set<Socket> mSockets = new HashSet<Socket>(); 928 929 /** 930 * Defines the keystore contents for the server, BKS version. Holds just a 931 * single self-generated key. The subject name is "Test Server". 932 */ 933 private static final String SERVER_KEYS_BKS = 934 "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" + 935 "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" + 936 "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" + 937 "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" + 938 "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" + 939 "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" + 940 "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" + 941 "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" + 942 "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" + 943 "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" + 944 "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" + 945 "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" + 946 "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" + 947 "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" + 948 "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" + 949 "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" + 950 "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" + 951 "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" + 952 "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" + 953 "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" + 954 "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" + 955 "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" + 956 "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" + 957 "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw="; 958 959 private static final String PASSWORD = "android"; 960 private static final char[] EMPTY_PASSWORD = new char[0]; 961 962 /** 963 * Loads a keystore from a base64-encoded String. Returns the KeyManager[] 964 * for the result. 965 */ getHardCodedKeyManagers()966 private static KeyManager[] getHardCodedKeyManagers() throws Exception { 967 byte[] bytes = Base64.decode(SERVER_KEYS_BKS.getBytes(), Base64.DEFAULT); 968 InputStream inputStream = new ByteArrayInputStream(bytes); 969 970 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 971 keyStore.load(inputStream, PASSWORD.toCharArray()); 972 inputStream.close(); 973 974 String algorithm = KeyManagerFactory.getDefaultAlgorithm(); 975 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); 976 keyManagerFactory.init(keyStore, PASSWORD.toCharArray()); 977 978 return keyManagerFactory.getKeyManagers(); 979 } 980 getKeyManagersFromStreams(InputStream key, InputStream cert)981 private KeyManager[] getKeyManagersFromStreams(InputStream key, InputStream cert) 982 throws Exception { 983 ByteArrayOutputStream os = new ByteArrayOutputStream(); 984 byte[] buffer = new byte[4096]; 985 int n; 986 while ((n = key.read(buffer, 0, buffer.length)) != -1) { 987 os.write(buffer, 0, n); 988 } 989 key.close(); 990 byte[] keyBytes = os.toByteArray(); 991 KeyFactory kf = KeyFactory.getInstance("RSA"); 992 Key privKey = kf.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); 993 994 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 995 Certificate[] chain = new Certificate[1]; 996 chain[0] = cf.generateCertificate(cert); 997 998 KeyStore keyStore = KeyStore.getInstance("PKCS12"); 999 keyStore.load(/*stream=*/null, /*password*/null); 1000 keyStore.setKeyEntry("server", privKey, EMPTY_PASSWORD, chain); 1001 1002 String algorithm = KeyManagerFactory.getDefaultAlgorithm(); 1003 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); 1004 keyManagerFactory.init(keyStore, EMPTY_PASSWORD); 1005 return keyManagerFactory.getKeyManagers(); 1006 } 1007 ServerThread(CtsTestServer server, SslMode sslMode, InputStream key, InputStream cert)1008 ServerThread(CtsTestServer server, SslMode sslMode, InputStream key, 1009 InputStream cert) throws Exception { 1010 super("ServerThread"); 1011 mServer = server; 1012 mSsl = sslMode; 1013 KeyManager[] keyManagers; 1014 if (key == null && cert == null) { 1015 keyManagers = getHardCodedKeyManagers(); 1016 } else { 1017 keyManagers = getKeyManagersFromStreams(key, cert); 1018 } 1019 int retry = 3; 1020 while (true) { 1021 try { 1022 if (mSsl == SslMode.INSECURE) { 1023 mSocket = new ServerSocket(0); 1024 } else { // Use SSL 1025 mSslContext = SSLContext.getInstance("TLS"); 1026 mSslContext.init(keyManagers, mServer.getTrustManagers(), null); 1027 mSocket = mSslContext.getServerSocketFactory().createServerSocket(0); 1028 if (mSsl == SslMode.TRUST_ANY_CLIENT) { 1029 HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 1030 @Override 1031 public boolean verify(String s, SSLSession sslSession) { 1032 return true; 1033 } 1034 }); 1035 HttpsURLConnection.setDefaultSSLSocketFactory( 1036 mSslContext.getSocketFactory()); 1037 } else if (mSsl == SslMode.WANTS_CLIENT_AUTH) { 1038 ((SSLServerSocket) mSocket).setWantClientAuth(true); 1039 } else if (mSsl == SslMode.NEEDS_CLIENT_AUTH) { 1040 ((SSLServerSocket) mSocket).setNeedClientAuth(true); 1041 } 1042 } 1043 return; 1044 } catch (IOException e) { 1045 if (--retry == 0) { 1046 throw e; 1047 } 1048 // sleep in case server socket is still being closed 1049 Thread.sleep(1000); 1050 } 1051 } 1052 } 1053 run()1054 public void run() { 1055 while (!mWillShutDown) { 1056 try { 1057 Socket socket = mSocket.accept(); 1058 1059 synchronized(mLock) { 1060 mSockets.add(socket); 1061 } 1062 1063 DefaultHttpServerConnection conn = mServer.createHttpServerConnection(); 1064 HttpParams params = new BasicHttpParams(); 1065 params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0); 1066 conn.bind(socket, params); 1067 1068 // Determine whether we need to shutdown early before 1069 // parsing the response since conn.close() will crash 1070 // for SSL requests due to UnsupportedOperationException. 1071 HttpRequest request = conn.receiveRequestHeader(); 1072 if (request instanceof HttpEntityEnclosingRequest) { 1073 conn.receiveRequestEntity( (HttpEntityEnclosingRequest) request); 1074 } 1075 1076 mExecutorService.execute(new HandleResponseTask(conn, request, socket)); 1077 } catch (IOException e) { 1078 // normal during shutdown, ignore 1079 Log.w(TAG, e); 1080 } catch (RejectedExecutionException e) { 1081 // normal during shutdown, ignore 1082 Log.w(TAG, e); 1083 } catch (HttpException e) { 1084 Log.w(TAG, e); 1085 } catch (UnsupportedOperationException e) { 1086 // DefaultHttpServerConnection's close() throws an 1087 // UnsupportedOperationException. 1088 Log.w(TAG, e); 1089 } 1090 } 1091 } 1092 1093 /** 1094 * Shutdown the socket and the executor service. 1095 * Note this method is called on the client thread, instead of the server thread. 1096 */ shutDownOnClientThread()1097 public void shutDownOnClientThread() { 1098 try { 1099 mWillShutDown = true; 1100 mExecutorService.shutdown(); 1101 mExecutorService.awaitTermination(1L, TimeUnit.MINUTES); 1102 mSocket.close(); 1103 // To prevent the server thread from being blocked on read from socket, 1104 // which is called when the server tries to receiveRequestHeader, 1105 // close all the sockets here. 1106 synchronized(mLock) { 1107 for (Socket socket : mSockets) { 1108 socket.close(); 1109 } 1110 } 1111 } catch (IOException ignored) { 1112 // safe to ignore 1113 } catch (InterruptedException e) { 1114 Log.e(TAG, "Shutting down threads", e); 1115 } 1116 } 1117 1118 private class HandleResponseTask implements Runnable { 1119 1120 private DefaultHttpServerConnection mConnection; 1121 1122 private HttpRequest mRequest; 1123 1124 private Socket mSocket; 1125 HandleResponseTask(DefaultHttpServerConnection connection, HttpRequest request, Socket socket)1126 public HandleResponseTask(DefaultHttpServerConnection connection, 1127 HttpRequest request, Socket socket) { 1128 this.mConnection = connection; 1129 this.mRequest = request; 1130 this.mSocket = socket; 1131 } 1132 1133 @Override run()1134 public void run() { 1135 try { 1136 HttpResponse response = mServer.getResponse(mRequest); 1137 mConnection.sendResponseHeader(response); 1138 mConnection.sendResponseEntity(response); 1139 mConnection.close(); 1140 1141 synchronized(mLock) { 1142 ServerThread.this.mSockets.remove(mSocket); 1143 } 1144 } catch (Exception e) { 1145 Log.e(TAG, "Error handling request:", e); 1146 } 1147 } 1148 } 1149 } 1150 } 1151