Home | History | Annotate | Download | only in cts
      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