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