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