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