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