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