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