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