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