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