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