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