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