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