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