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