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