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