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