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