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