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