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