| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.webkit.cts; |
| |
| import android.content.Context; |
| import android.content.res.AssetManager; |
| import android.content.res.Resources; |
| import android.net.Uri; |
| import android.os.Environment; |
| import android.util.Base64; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.webkit.MimeTypeMap; |
| |
| import org.apache.http.Header; |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpEntityEnclosingRequest; |
| import org.apache.http.HttpException; |
| import org.apache.http.HttpRequest; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.HttpVersion; |
| import org.apache.http.NameValuePair; |
| import org.apache.http.RequestLine; |
| import org.apache.http.StatusLine; |
| import org.apache.http.client.utils.URLEncodedUtils; |
| import org.apache.http.entity.ByteArrayEntity; |
| import org.apache.http.entity.FileEntity; |
| import org.apache.http.entity.InputStreamEntity; |
| import org.apache.http.entity.StringEntity; |
| import org.apache.http.impl.DefaultHttpServerConnection; |
| import org.apache.http.impl.cookie.DateUtils; |
| import org.apache.http.message.BasicHttpResponse; |
| import org.apache.http.params.BasicHttpParams; |
| import org.apache.http.params.CoreProtocolPNames; |
| import org.apache.http.params.HttpParams; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.net.URI; |
| import java.net.URLEncoder; |
| import java.security.Key; |
| import java.security.KeyFactory; |
| import java.security.KeyStore; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.security.spec.PKCS8EncodedKeySpec; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Vector; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.RejectedExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.KeyManager; |
| import javax.net.ssl.KeyManagerFactory; |
| import javax.net.ssl.SSLContext; |
| import javax.net.ssl.SSLServerSocket; |
| import javax.net.ssl.SSLSession; |
| import javax.net.ssl.X509TrustManager; |
| |
| /** |
| * Simple http test server for testing webkit client functionality. |
| */ |
| public class CtsTestServer { |
| private static final String TAG = "CtsTestServer"; |
| |
| public static final String FAVICON_PATH = "/favicon.ico"; |
| public static final String USERAGENT_PATH = "/useragent.html"; |
| |
| public static final String TEST_DOWNLOAD_PATH = "/download.html"; |
| public static final String CACHEABLE_TEST_DOWNLOAD_PATH = |
| "/cacheable-download.html"; |
| private static final String DOWNLOAD_ID_PARAMETER = "downloadId"; |
| private static final String NUM_BYTES_PARAMETER = "numBytes"; |
| |
| private static final String ASSET_PREFIX = "/assets/"; |
| private static final String RAW_PREFIX = "raw/"; |
| private static final String FAVICON_ASSET_PATH = ASSET_PREFIX + "webkit/favicon.png"; |
| private static final String APPCACHE_PATH = "/appcache.html"; |
| private static final String APPCACHE_MANIFEST_PATH = "/appcache.manifest"; |
| private static final String REDIRECT_PREFIX = "/redirect"; |
| private static final String QUERY_REDIRECT_PATH = "/alt_redirect"; |
| private static final String ECHO_HEADERS_PREFIX = "/echo_headers"; |
| private static final String DELAY_PREFIX = "/delayed"; |
| private static final String BINARY_PREFIX = "/binary"; |
| private static final String SET_COOKIE_PREFIX = "/setcookie"; |
| private static final String COOKIE_PREFIX = "/cookie"; |
| private static final String LINKED_SCRIPT_PREFIX = "/linkedscriptprefix"; |
| private static final String AUTH_PREFIX = "/auth"; |
| public static final String NOLENGTH_POSTFIX = "nolength"; |
| private static final int DELAY_MILLIS = 2000; |
| |
| public static final String ECHOED_RESPONSE_HEADER_PREFIX = "x-request-header-"; |
| |
| public static final String AUTH_REALM = "Android CTS"; |
| public static final String AUTH_USER = "cts"; |
| public static final String AUTH_PASS = "secret"; |
| // base64 encoded credentials "cts:secret" used for basic authentication |
| public static final String AUTH_CREDENTIALS = "Basic Y3RzOnNlY3JldA=="; |
| |
| public static final String MESSAGE_401 = "401 unauthorized"; |
| public static final String MESSAGE_403 = "403 forbidden"; |
| public static final String MESSAGE_404 = "404 not found"; |
| |
| private static Hashtable<Integer, String> sReasons; |
| |
| private ServerThread mServerThread; |
| private String mServerUri; |
| private AssetManager mAssets; |
| private Context mContext; |
| private Resources mResources; |
| private @SslMode int mSsl; |
| private MimeTypeMap mMap; |
| private Vector<String> mQueries; |
| private ArrayList<HttpEntity> mRequestEntities; |
| private final Map<String, Integer> mRequestCountMap = new HashMap<String, Integer>(); |
| private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>(); |
| private final Map<String, HttpResponse> mResponseMap = new HashMap<String, HttpResponse>(); |
| private long mDocValidity; |
| private long mDocAge; |
| private X509TrustManager mTrustManager; |
| |
| /** |
| * Create and start a local HTTP server instance. |
| * @param context The application context to use for fetching assets. |
| * @throws IOException |
| */ |
| public CtsTestServer(Context context) throws Exception { |
| this(context, SslMode.INSECURE); |
| } |
| |
| public static String getReasonString(int status) { |
| if (sReasons == null) { |
| sReasons = new Hashtable<Integer, String>(); |
| sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized"); |
| sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found"); |
| sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden"); |
| sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily"); |
| } |
| return sReasons.get(status); |
| } |
| |
| /** |
| * Create and start a local HTTP server instance. |
| * @param context The application context to use for fetching assets. |
| * @param ssl True if the server should be using secure sockets. |
| * @throws Exception |
| */ |
| public CtsTestServer(Context context, boolean ssl) throws Exception { |
| this(context, ssl ? SslMode.NO_CLIENT_AUTH : SslMode.INSECURE); |
| } |
| |
| /** |
| * Create and start a local HTTP server instance. |
| * @param context The application context to use for fetching assets. |
| * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. |
| * @throws Exception |
| */ |
| public CtsTestServer(Context context, @SslMode int sslMode) throws Exception { |
| this(context, sslMode, 0, 0); |
| } |
| |
| /** |
| * Create and start a local HTTP server instance. |
| * @param context The application context to use for fetching assets. |
| * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. |
| * @param trustManager the trustManager |
| * @throws Exception |
| */ |
| public CtsTestServer(Context context, @SslMode int sslMode, X509TrustManager trustManager) |
| throws Exception { |
| this(context, sslMode, trustManager, 0, 0); |
| } |
| |
| /** |
| * Create and start a local HTTP server instance. |
| * @param context The application context to use for fetching assets. |
| * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. |
| * @param keyResId Raw resource ID of the server private key to use. |
| * @param certResId Raw resource ID of the server certificate to use. |
| * @throws Exception |
| */ |
| public CtsTestServer(Context context, @SslMode int sslMode, int keyResId, int certResId) |
| throws Exception { |
| this(context, sslMode, new CtsTrustManager(), keyResId, certResId); |
| } |
| |
| /** |
| * Create and start a local HTTP server instance. |
| * @param context The application context to use for fetching assets. |
| * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use. |
| * @param trustManager the trustManager |
| * @param keyResId Raw resource ID of the server private key to use. |
| * @param certResId Raw resource ID of the server certificate to use. |
| * @throws Exception |
| */ |
| public CtsTestServer(Context context, @SslMode int sslMode, X509TrustManager trustManager, |
| int keyResId, int certResId) throws Exception { |
| mContext = context; |
| mAssets = mContext.getAssets(); |
| mResources = mContext.getResources(); |
| mSsl = sslMode; |
| mRequestEntities = new ArrayList<HttpEntity>(); |
| mMap = MimeTypeMap.getSingleton(); |
| mQueries = new Vector<String>(); |
| mTrustManager = trustManager; |
| if (keyResId == 0 && certResId == 0) { |
| mServerThread = new ServerThread(this, mSsl, null, null); |
| } else { |
| mServerThread = new ServerThread(this, mSsl, mResources.openRawResource(keyResId), |
| mResources.openRawResource(certResId)); |
| } |
| if (mSsl == SslMode.INSECURE) { |
| mServerUri = "http:"; |
| } else { |
| mServerUri = "https:"; |
| } |
| mServerUri += "//localhost:" + mServerThread.mSocket.getLocalPort(); |
| mServerThread.start(); |
| } |
| |
| /** |
| * Terminate the http server. |
| */ |
| public void shutdown() { |
| mServerThread.shutDownOnClientThread(); |
| |
| try { |
| // Block until the server thread is done shutting down. |
| mServerThread.join(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * {@link X509TrustManager} that trusts everybody. This is used so that |
| * the client calling {@link CtsTestServer#shutdown()} can issue a request |
| * for shutdown by blindly trusting the {@link CtsTestServer}'s |
| * credentials. |
| */ |
| static class CtsTrustManager implements X509TrustManager { |
| public void checkClientTrusted(X509Certificate[] chain, String authType) { |
| // Trust the CtSTestServer's client... |
| } |
| |
| public void checkServerTrusted(X509Certificate[] chain, String authType) { |
| // Trust the CtSTestServer... |
| } |
| |
| public X509Certificate[] getAcceptedIssuers() { |
| return null; |
| } |
| } |
| |
| /** |
| * @return a trust manager array of size 1. |
| */ |
| private X509TrustManager[] getTrustManagers() { |
| return new X509TrustManager[] { mTrustManager }; |
| } |
| |
| /** |
| * {@link HostnameVerifier} that verifies everybody. This permits |
| * the client to trust the web server and call |
| * {@link CtsTestServer#shutdown()}. |
| */ |
| private static class CtsHostnameVerifier implements HostnameVerifier { |
| public boolean verify(String hostname, SSLSession session) { |
| return true; |
| } |
| } |
| |
| /** |
| * Sets a response to be returned when a particular request path is passed in (with the option |
| * to specify additional headers). |
| * |
| * @param requestPath The path to respond to. |
| * @param responseString The response body that will be returned. |
| * @param responseHeaders Any additional headers that should be returned along with the response |
| * (null is acceptable). |
| * @return The full URL including the path that should be requested to get the expected |
| * response. |
| */ |
| public synchronized String setResponse( |
| String requestPath, String responseString, List<Pair<String, String>> responseHeaders) { |
| HttpResponse response = createResponse(HttpStatus.SC_OK); |
| response.setEntity(createEntity(responseString)); |
| if (responseHeaders != null) { |
| for (Pair<String, String> headerPair : responseHeaders) { |
| response.setHeader(headerPair.first, headerPair.second); |
| } |
| } |
| mResponseMap.put(requestPath, response); |
| |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(requestPath); |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Return the URI that points to the server root. |
| */ |
| public String getBaseUri() { |
| return mServerUri; |
| } |
| |
| /** |
| * Return the absolute URL that refers to a path. |
| */ |
| public String getAbsoluteUrl(String path) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| /** |
| * Return the absolute URL that refers to the given asset. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| */ |
| public String getAssetUrl(String path) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(ASSET_PREFIX); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| /** |
| * Return an artificially delayed absolute URL that refers to the given asset. This can be |
| * used to emulate a slow HTTP server or connection. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| */ |
| public String getDelayedAssetUrl(String path) { |
| return getDelayedAssetUrl(path, DELAY_MILLIS); |
| } |
| |
| /** |
| * Return an artificially delayed absolute URL that refers to the given asset. This can be |
| * used to emulate a slow HTTP server or connection. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| * @param delayMs The number of milliseconds to delay the request |
| */ |
| public String getDelayedAssetUrl(String path, int delayMs) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(DELAY_PREFIX); |
| sb.append("/"); |
| sb.append(delayMs); |
| sb.append(ASSET_PREFIX); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| /** |
| * Return an absolute URL that refers to the given asset and is protected by |
| * HTTP authentication. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| */ |
| public String getAuthAssetUrl(String path) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(AUTH_PREFIX); |
| sb.append(ASSET_PREFIX); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| /** |
| * Return an absolute URL that refers to an endpoint which will send received headers back to |
| * the sender with a prefix. |
| */ |
| public String getEchoHeadersUrl() { |
| return getBaseUri() + ECHO_HEADERS_PREFIX; |
| } |
| |
| /** |
| * Return an absolute URL that indirectly refers to the given asset. |
| * When a client fetches this URL, the server will respond with a temporary redirect (302) |
| * referring to the absolute URL of the given asset. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| */ |
| public String getRedirectingAssetUrl(String path) { |
| return getRedirectingAssetUrl(path, 1); |
| } |
| |
| /** |
| * Return an absolute URL that indirectly refers to the given asset. |
| * When a client fetches this URL, the server will respond with a temporary redirect (302) |
| * referring to the absolute URL of the given asset. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| * @param numRedirects The number of redirects required to reach the given asset. |
| */ |
| public String getRedirectingAssetUrl(String path, int numRedirects) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| for (int i = 0; i < numRedirects; i++) { |
| sb.append(REDIRECT_PREFIX); |
| } |
| sb.append(ASSET_PREFIX); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| /** |
| * Return an absolute URL that indirectly refers to the given asset, without having |
| * the destination path be part of the redirecting path. |
| * When a client fetches this URL, the server will respond with a temporary redirect (302) |
| * referring to the absolute URL of the given asset. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| */ |
| public String getQueryRedirectingAssetUrl(String path) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(QUERY_REDIRECT_PATH); |
| sb.append("?dest="); |
| try { |
| sb.append(URLEncoder.encode(getAssetUrl(path), "UTF-8")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * getSetCookieUrl returns a URL that attempts to set the cookie |
| * "key=value" when fetched. |
| * @param path a suffix to disambiguate multiple Cookie URLs. |
| * @param key the key of the cookie. |
| * @return the url for a page that attempts to set the cookie. |
| */ |
| public String getSetCookieUrl(String path, String key, String value) { |
| return getSetCookieUrl(path, key, value, null); |
| } |
| |
| /** |
| * getSetCookieUrl returns a URL that attempts to set the cookie |
| * "key=value" with the given list of attributes when fetched. |
| * @param path a suffix to disambiguate multiple Cookie URLs. |
| * @param key the key of the cookie |
| * @param attributes the attributes to set |
| * @return the url for a page that attempts to set the cookie. |
| */ |
| public String getSetCookieUrl(String path, String key, String value, String attributes) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(SET_COOKIE_PREFIX); |
| sb.append(path); |
| sb.append("?key="); |
| sb.append(key); |
| sb.append("&value="); |
| sb.append(value); |
| if (attributes != null) { |
| sb.append("&attributes="); |
| sb.append(attributes); |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * getLinkedScriptUrl returns a URL for a page with a script tag where |
| * src equals the URL passed in. |
| * @param path a suffix to disambiguate mulitple Linked Script URLs. |
| * @param url the src of the script tag. |
| * @return the url for the page with the script link in. |
| */ |
| public String getLinkedScriptUrl(String path, String url) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(LINKED_SCRIPT_PREFIX); |
| sb.append(path); |
| sb.append("?url="); |
| try { |
| sb.append(URLEncoder.encode(url, "UTF-8")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| return sb.toString(); |
| } |
| |
| public String getBinaryUrl(String mimeType, int contentLength) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(BINARY_PREFIX); |
| sb.append("?type="); |
| sb.append(mimeType); |
| sb.append("&length="); |
| sb.append(contentLength); |
| return sb.toString(); |
| } |
| |
| public String getCookieUrl(String path) { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(COOKIE_PREFIX); |
| sb.append("/"); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| public String getUserAgentUrl() { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(USERAGENT_PATH); |
| return sb.toString(); |
| } |
| |
| public String getAppCacheUrl() { |
| StringBuilder sb = new StringBuilder(getBaseUri()); |
| sb.append(APPCACHE_PATH); |
| return sb.toString(); |
| } |
| |
| /** |
| * @param downloadId used to differentiate the files created for each test |
| * @param numBytes of the content that the CTS server should send back |
| * @return url to get the file from |
| */ |
| public String getTestDownloadUrl(String downloadId, int numBytes) { |
| return Uri.parse(getBaseUri()) |
| .buildUpon() |
| .path(TEST_DOWNLOAD_PATH) |
| .appendQueryParameter(DOWNLOAD_ID_PARAMETER, downloadId) |
| .appendQueryParameter(NUM_BYTES_PARAMETER, Integer.toString(numBytes)) |
| .build() |
| .toString(); |
| } |
| |
| /** |
| * @param downloadId used to differentiate the files created for each test |
| * @param numBytes of the content that the CTS server should send back |
| * @return url to get the file from |
| */ |
| public String getCacheableTestDownloadUrl(String downloadId, int numBytes) { |
| return Uri.parse(getBaseUri()) |
| .buildUpon() |
| .path(CACHEABLE_TEST_DOWNLOAD_PATH) |
| .appendQueryParameter(DOWNLOAD_ID_PARAMETER, downloadId) |
| .appendQueryParameter(NUM_BYTES_PARAMETER, Integer.toString(numBytes)) |
| .build() |
| .toString(); |
| } |
| |
| /** |
| * Returns true if the resource identified by url has been requested since |
| * the server was started or the last call to resetRequestState(). |
| * |
| * @param url The relative url to check whether it has been requested. |
| */ |
| public synchronized boolean wasResourceRequested(String url) { |
| Iterator<String> it = mQueries.iterator(); |
| while (it.hasNext()) { |
| String request = it.next(); |
| if (request.endsWith(url)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns all received request entities since the last reset. |
| */ |
| public synchronized ArrayList<HttpEntity> getRequestEntities() { |
| return mRequestEntities; |
| } |
| |
| /** |
| * Returns the total number of requests made. |
| */ |
| public synchronized int getRequestCount() { |
| return mQueries.size(); |
| } |
| |
| /** |
| * Returns the number of requests made for a path. |
| */ |
| public synchronized int getRequestCount(String requestPath) { |
| Integer count = mRequestCountMap.get(requestPath); |
| if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath); |
| return count.intValue(); |
| } |
| |
| /** |
| * Set the validity of any future responses in milliseconds. If this is set to a non-zero |
| * value, the server will include a "Expires" header. |
| * @param timeMillis The time, in milliseconds, for which any future response will be valid. |
| */ |
| public synchronized void setDocumentValidity(long timeMillis) { |
| mDocValidity = timeMillis; |
| } |
| |
| /** |
| * Set the age of documents served. If this is set to a non-zero value, the server will include |
| * a "Last-Modified" header calculated from the value. |
| * @param timeMillis The age, in milliseconds, of any document served in the future. |
| */ |
| public synchronized void setDocumentAge(long timeMillis) { |
| mDocAge = timeMillis; |
| } |
| |
| /** |
| * Resets the saved requests and request counts. |
| */ |
| public synchronized void resetRequestState() { |
| |
| mQueries.clear(); |
| mRequestEntities = new ArrayList<HttpEntity>(); |
| } |
| |
| /** |
| * Returns the last HttpRequest at this path. |
| * Can return null if it is never requested. |
| * |
| * Use this method if the request you're looking for was |
| * for an asset. |
| */ |
| public HttpRequest getLastAssetRequest(String requestPath) { |
| String relativeUrl = getRelativeAssetUrl(requestPath); |
| return mLastRequestMap.get(relativeUrl); |
| } |
| |
| /** |
| * Returns the last HttpRequest at this path. |
| * Can return null if it is never requested. |
| */ |
| public HttpRequest getLastRequest(String requestPath) { |
| return mLastRequestMap.get(requestPath); |
| } |
| |
| /** |
| * Hook for adding stuffs for HTTP POST. Default implementation does nothing. |
| * @return null to use the default response mechanism of sending the requested uri as it is. |
| * Otherwise, the whole response should be handled inside onPost. |
| */ |
| protected HttpResponse onPost(HttpRequest request) throws Exception { |
| return null; |
| } |
| |
| /** |
| * Return the relative URL that refers to the given asset. |
| * @param path The path of the asset. See {@link AssetManager#open(String)} |
| */ |
| private String getRelativeAssetUrl(String path) { |
| StringBuilder sb = new StringBuilder(ASSET_PREFIX); |
| sb.append(path); |
| return sb.toString(); |
| } |
| |
| /** |
| * Generate a response to the given request. |
| * @throws InterruptedException |
| * @throws IOException |
| */ |
| private HttpResponse getResponse(HttpRequest request) throws Exception { |
| RequestLine requestLine = request.getRequestLine(); |
| HttpResponse response = null; |
| String uriString = requestLine.getUri(); |
| Log.i(TAG, requestLine.getMethod() + ": " + uriString); |
| |
| synchronized (this) { |
| mQueries.add(uriString); |
| int requestCount = 0; |
| if (mRequestCountMap.containsKey(uriString)) { |
| requestCount = mRequestCountMap.get(uriString); |
| } |
| mRequestCountMap.put(uriString, requestCount + 1); |
| mLastRequestMap.put(uriString, request); |
| if (request instanceof HttpEntityEnclosingRequest) { |
| mRequestEntities.add(((HttpEntityEnclosingRequest)request).getEntity()); |
| } |
| } |
| |
| if (requestLine.getMethod().equals("POST")) { |
| HttpResponse responseOnPost = onPost(request); |
| if (responseOnPost != null) { |
| return responseOnPost; |
| } |
| } |
| |
| URI uri = URI.create(uriString); |
| String path = uri.getPath(); |
| String query = uri.getQuery(); |
| |
| if (path.startsWith(ECHO_HEADERS_PREFIX)) { |
| response = createResponse(HttpStatus.SC_OK); |
| for (Header header : request.getAllHeaders()) { |
| response.addHeader( |
| ECHOED_RESPONSE_HEADER_PREFIX + header.getName(), header.getValue()); |
| } |
| } |
| if (path.equals(FAVICON_PATH)) { |
| path = FAVICON_ASSET_PATH; |
| } |
| if (path.startsWith(DELAY_PREFIX)) { |
| String delayPath = path.substring(DELAY_PREFIX.length() + 1); |
| String delay = delayPath.substring(0, delayPath.indexOf('/')); |
| path = delayPath.substring(delay.length()); |
| try { |
| Thread.sleep(Integer.valueOf(delay)); |
| } catch (InterruptedException ignored) { |
| // ignore |
| } |
| } |
| if (path.startsWith(AUTH_PREFIX)) { |
| // authentication required |
| Header[] auth = request.getHeaders("Authorization"); |
| if ((auth.length > 0 && auth[0].getValue().equals(AUTH_CREDENTIALS)) |
| // This is a hack to make sure that loads to this url's will always |
| // ask for authentication. This is what the test expects. |
| && !path.endsWith("embedded_image.html")) { |
| // fall through and serve content |
| path = path.substring(AUTH_PREFIX.length()); |
| } else { |
| // request authorization |
| response = createResponse(HttpStatus.SC_UNAUTHORIZED); |
| response.addHeader("WWW-Authenticate", "Basic realm=\"" + AUTH_REALM + "\""); |
| } |
| } |
| if (path.startsWith(BINARY_PREFIX)) { |
| List <NameValuePair> args = URLEncodedUtils.parse(uri, "UTF-8"); |
| int length = 0; |
| String mimeType = null; |
| try { |
| for (NameValuePair pair : args) { |
| String name = pair.getName(); |
| if (name.equals("type")) { |
| mimeType = pair.getValue(); |
| } else if (name.equals("length")) { |
| length = Integer.parseInt(pair.getValue()); |
| } |
| } |
| if (length > 0 && mimeType != null) { |
| ByteArrayEntity entity = new ByteArrayEntity(new byte[length]); |
| entity.setContentType(mimeType); |
| response = createResponse(HttpStatus.SC_OK); |
| response.setEntity(entity); |
| response.addHeader("Content-Disposition", "attachment; filename=test.bin"); |
| response.addHeader("Content-Type", mimeType); |
| response.addHeader("Content-Length", "" + length); |
| } else { |
| // fall through, return 404 at the end |
| } |
| } catch (Exception e) { |
| // fall through, return 404 at the end |
| Log.w(TAG, e); |
| } |
| } else if (path.startsWith(ASSET_PREFIX)) { |
| path = path.substring(ASSET_PREFIX.length()); |
| // request for an asset file |
| try { |
| InputStream in; |
| if (path.startsWith(RAW_PREFIX)) { |
| String resourceName = path.substring(RAW_PREFIX.length()); |
| int id = mResources.getIdentifier(resourceName, "raw", mContext.getPackageName()); |
| if (id == 0) { |
| Log.w(TAG, "Can't find raw resource " + resourceName); |
| throw new IOException(); |
| } |
| in = mResources.openRawResource(id); |
| } else if (path.startsWith( |
| Environment.getExternalStorageDirectory().getAbsolutePath())) { |
| in = new FileInputStream(path); |
| } else { |
| in = mAssets.open(path); |
| } |
| response = createResponse(HttpStatus.SC_OK); |
| InputStreamEntity entity = new InputStreamEntity(in, in.available()); |
| String mimeType = |
| mMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(path)); |
| if (mimeType == null) { |
| mimeType = "text/html"; |
| } |
| entity.setContentType(mimeType); |
| response.setEntity(entity); |
| if (query == null || !query.contains(NOLENGTH_POSTFIX)) { |
| response.setHeader("Content-Length", "" + entity.getContentLength()); |
| } |
| } catch (IOException | NullPointerException e) { |
| response = null; |
| // fall through, return 404 at the end |
| } |
| } else if (path.startsWith(REDIRECT_PREFIX)) { |
| response = createResponse(HttpStatus.SC_MOVED_TEMPORARILY); |
| String location = getBaseUri() + path.substring(REDIRECT_PREFIX.length()); |
| Log.i(TAG, "Redirecting to: " + location); |
| response.addHeader("Location", location); |
| } else if (path.equals(QUERY_REDIRECT_PATH)) { |
| Uri androidUri = Uri.parse(uriString); |
| String location = androidUri.getQueryParameter("dest"); |
| |
| int statusCode = HttpStatus.SC_MOVED_TEMPORARILY; |
| String statusCodeParam = androidUri.getQueryParameter("statusCode"); |
| if (statusCodeParam != null) { |
| try { |
| int parsedStatusCode = Integer.parseInt(statusCodeParam); |
| if (300 <= parsedStatusCode && parsedStatusCode < 400) { |
| statusCode = parsedStatusCode; |
| } |
| } catch (NumberFormatException ignored) { } |
| } |
| |
| if (location != null) { |
| Log.i(TAG, "Redirecting to: " + location); |
| response = createResponse(statusCode); |
| response.addHeader("Location", location); |
| } |
| } else if (path.startsWith(COOKIE_PREFIX)) { |
| /* |
| * Return a page with a title containing a list of all incoming cookies, |
| * separated by '|' characters. If a numeric 'count' value is passed in a cookie, |
| * return a cookie with the value incremented by 1. Otherwise, return a cookie |
| * setting 'count' to 0. |
| */ |
| response = createResponse(HttpStatus.SC_OK); |
| Header[] cookies = request.getHeaders("Cookie"); |
| Pattern p = Pattern.compile("count=(\\d+)"); |
| StringBuilder cookieString = new StringBuilder(100); |
| cookieString.append(cookies.length); |
| int count = 0; |
| for (Header cookie : cookies) { |
| cookieString.append("|"); |
| String value = cookie.getValue(); |
| cookieString.append(value); |
| Matcher m = p.matcher(value); |
| if (m.find()) { |
| count = Integer.parseInt(m.group(1)) + 1; |
| } |
| } |
| |
| response.addHeader("Set-Cookie", "count=" + count + "; path=" + COOKIE_PREFIX); |
| response.setEntity(createPage(cookieString.toString(), cookieString.toString())); |
| } else if (path.startsWith(SET_COOKIE_PREFIX)) { |
| response = createResponse(HttpStatus.SC_OK); |
| Uri parsedUri = Uri.parse(uriString); |
| String key = parsedUri.getQueryParameter("key"); |
| String value = parsedUri.getQueryParameter("value"); |
| String attributes = parsedUri.getQueryParameter("attributes"); |
| String cookie = key + "=" + value; |
| if (attributes != null) { |
| cookie = cookie + "; " + attributes; |
| } |
| response.addHeader("Set-Cookie", cookie); |
| response.setEntity(createPage(cookie, cookie)); |
| } else if (path.startsWith(LINKED_SCRIPT_PREFIX)) { |
| response = createResponse(HttpStatus.SC_OK); |
| String src = Uri.parse(uriString).getQueryParameter("url"); |
| String scriptTag = "<script src=\"" + src + "\"></script>"; |
| response.setEntity(createPage("LinkedScript", scriptTag)); |
| } else if (path.equals(USERAGENT_PATH)) { |
| response = createResponse(HttpStatus.SC_OK); |
| Header agentHeader = request.getFirstHeader("User-Agent"); |
| String agent = ""; |
| if (agentHeader != null) { |
| agent = agentHeader.getValue(); |
| } |
| response.setEntity(createPage(agent, agent)); |
| } else if (path.equals(TEST_DOWNLOAD_PATH)) { |
| response = createTestDownloadResponse(mContext, Uri.parse(uriString)); |
| } else if (path.equals(CACHEABLE_TEST_DOWNLOAD_PATH)) { |
| response = createCacheableTestDownloadResponse(mContext, Uri.parse(uriString)); |
| } else if (path.equals(APPCACHE_PATH)) { |
| response = createResponse(HttpStatus.SC_OK); |
| response.setEntity(createEntity("<!DOCTYPE HTML>" + |
| "<html manifest=\"appcache.manifest\">" + |
| " <head>" + |
| " <title>Waiting</title>" + |
| " <script>" + |
| " function updateTitle(x) { document.title = x; }" + |
| " window.applicationCache.onnoupdate = " + |
| " function() { updateTitle(\"onnoupdate Callback\"); };" + |
| " window.applicationCache.oncached = " + |
| " function() { updateTitle(\"oncached Callback\"); };" + |
| " window.applicationCache.onupdateready = " + |
| " function() { updateTitle(\"onupdateready Callback\"); };" + |
| " window.applicationCache.onobsolete = " + |
| " function() { updateTitle(\"onobsolete Callback\"); };" + |
| " window.applicationCache.onerror = " + |
| " function() { updateTitle(\"onerror Callback\"); };" + |
| " </script>" + |
| " </head>" + |
| " <body onload=\"updateTitle('Loaded');\">AppCache test</body>" + |
| "</html>")); |
| } else if (path.equals(APPCACHE_MANIFEST_PATH)) { |
| response = createResponse(HttpStatus.SC_OK); |
| try { |
| StringEntity entity = new StringEntity("CACHE MANIFEST"); |
| // This entity property is not used when constructing the response, (See |
| // AbstractMessageWriter.write(), which is called by |
| // AbstractHttpServerConnection.sendResponseHeader()) so we have to set this header |
| // manually. |
| // TODO: Should we do this for all responses from this server? |
| entity.setContentType("text/cache-manifest"); |
| response.setEntity(entity); |
| response.setHeader("Content-Type", "text/cache-manifest"); |
| } catch (UnsupportedEncodingException e) { |
| Log.w(TAG, "Unexpected UnsupportedEncodingException"); |
| } |
| } |
| |
| // If a response was set, it should override whatever was generated |
| if (mResponseMap.containsKey(path)) { |
| response = mResponseMap.get(path); |
| } |
| |
| if (response == null) { |
| response = createResponse(HttpStatus.SC_NOT_FOUND); |
| } |
| StatusLine sl = response.getStatusLine(); |
| Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")"); |
| setDateHeaders(response); |
| return response; |
| } |
| |
| private void setDateHeaders(HttpResponse response) { |
| long time = System.currentTimeMillis(); |
| synchronized (this) { |
| if (mDocValidity != 0) { |
| String expires = DateUtils.formatDate(new Date(time + mDocValidity), |
| DateUtils.PATTERN_RFC1123); |
| response.addHeader("Expires", expires); |
| } |
| if (mDocAge != 0) { |
| String modified = DateUtils.formatDate(new Date(time - mDocAge), |
| DateUtils.PATTERN_RFC1123); |
| response.addHeader("Last-Modified", modified); |
| } |
| } |
| response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123)); |
| } |
| |
| /** |
| * Create an empty response with the given status. |
| */ |
| private static HttpResponse createResponse(int status) { |
| HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null); |
| |
| // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is Locale-dependent. |
| String reason = getReasonString(status); |
| if (reason != null) { |
| response.setEntity(createPage(reason, reason)); |
| } |
| return response; |
| } |
| |
| /** |
| * Create a string entity for the given content. |
| */ |
| private static StringEntity createEntity(String content) { |
| try { |
| StringEntity entity = new StringEntity(content); |
| entity.setContentType("text/html"); |
| return entity; |
| } catch (UnsupportedEncodingException e) { |
| Log.w(TAG, e); |
| } |
| return null; |
| } |
| |
| /** |
| * Create a string entity for a bare bones html page with provided title and body. |
| */ |
| private static StringEntity createPage(String title, String bodyContent) { |
| return createEntity("<html><head><title>" + title + "</title></head>" + |
| "<body>" + bodyContent + "</body></html>"); |
| } |
| |
| private static HttpResponse createTestDownloadResponse(Context context, Uri uri) |
| throws IOException { |
| String downloadId = uri.getQueryParameter(DOWNLOAD_ID_PARAMETER); |
| int numBytes = uri.getQueryParameter(NUM_BYTES_PARAMETER) != null |
| ? Integer.parseInt(uri.getQueryParameter(NUM_BYTES_PARAMETER)) |
| : 0; |
| HttpResponse response = createResponse(HttpStatus.SC_OK); |
| response.setHeader("Content-Length", Integer.toString(numBytes)); |
| response.setEntity(createFileEntity(context, downloadId, numBytes)); |
| return response; |
| } |
| |
| private static HttpResponse createCacheableTestDownloadResponse(Context context, Uri uri) |
| throws IOException { |
| HttpResponse response = createTestDownloadResponse(context, uri); |
| response.setHeader("Cache-Control", "max-age=300"); |
| return response; |
| } |
| |
| private static FileEntity createFileEntity(Context context, String downloadId, int numBytes) |
| throws IOException { |
| String storageState = Environment.getExternalStorageState(); |
| if (Environment.MEDIA_MOUNTED.equalsIgnoreCase(storageState)) { |
| File storageDir = context.getExternalFilesDir(null); |
| File file = new File(storageDir, downloadId + ".bin"); |
| BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file)); |
| byte data[] = new byte[1024]; |
| for (int i = 0; i < data.length; i++) { |
| data[i] = 1; |
| } |
| try { |
| for (int i = 0; i < numBytes / data.length; i++) { |
| stream.write(data); |
| } |
| stream.write(data, 0, numBytes % data.length); |
| stream.flush(); |
| } finally { |
| stream.close(); |
| } |
| return new FileEntity(file, "application/octet-stream"); |
| } else { |
| throw new IllegalStateException("External storage must be mounted for this test!"); |
| } |
| } |
| |
| protected DefaultHttpServerConnection createHttpServerConnection() { |
| return new DefaultHttpServerConnection(); |
| } |
| |
| private static class ServerThread extends Thread { |
| private CtsTestServer mServer; |
| private ServerSocket mSocket; |
| private @SslMode int mSsl; |
| private boolean mWillShutDown = false; |
| private SSLContext mSslContext; |
| private ExecutorService mExecutorService = Executors.newFixedThreadPool(20); |
| private Object mLock = new Object(); |
| // All the sockets bound to an open connection. |
| private Set<Socket> mSockets = new HashSet<Socket>(); |
| |
| /** |
| * Defines the keystore contents for the server, BKS version. Holds just a |
| * single self-generated key. The subject name is "Test Server". |
| */ |
| private static final String SERVER_KEYS_BKS = |
| "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" + |
| "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" + |
| "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" + |
| "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" + |
| "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" + |
| "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" + |
| "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" + |
| "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" + |
| "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" + |
| "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" + |
| "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" + |
| "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" + |
| "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" + |
| "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" + |
| "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" + |
| "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" + |
| "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" + |
| "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" + |
| "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" + |
| "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" + |
| "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" + |
| "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" + |
| "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" + |
| "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw="; |
| |
| private static final String PASSWORD = "android"; |
| private static final char[] EMPTY_PASSWORD = new char[0]; |
| |
| /** |
| * Loads a keystore from a base64-encoded String. Returns the KeyManager[] |
| * for the result. |
| */ |
| private static KeyManager[] getHardCodedKeyManagers() throws Exception { |
| byte[] bytes = Base64.decode(SERVER_KEYS_BKS.getBytes(), Base64.DEFAULT); |
| InputStream inputStream = new ByteArrayInputStream(bytes); |
| |
| KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| keyStore.load(inputStream, PASSWORD.toCharArray()); |
| inputStream.close(); |
| |
| String algorithm = KeyManagerFactory.getDefaultAlgorithm(); |
| KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); |
| keyManagerFactory.init(keyStore, PASSWORD.toCharArray()); |
| |
| return keyManagerFactory.getKeyManagers(); |
| } |
| |
| private KeyManager[] getKeyManagersFromStreams(InputStream key, InputStream cert) |
| throws Exception { |
| ByteArrayOutputStream os = new ByteArrayOutputStream(); |
| byte[] buffer = new byte[4096]; |
| int n; |
| while ((n = key.read(buffer, 0, buffer.length)) != -1) { |
| os.write(buffer, 0, n); |
| } |
| key.close(); |
| byte[] keyBytes = os.toByteArray(); |
| KeyFactory kf = KeyFactory.getInstance("RSA"); |
| Key privKey = kf.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); |
| |
| CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
| Certificate[] chain = new Certificate[1]; |
| chain[0] = cf.generateCertificate(cert); |
| |
| KeyStore keyStore = KeyStore.getInstance("PKCS12"); |
| keyStore.load(/*stream=*/null, /*password*/null); |
| keyStore.setKeyEntry("server", privKey, EMPTY_PASSWORD, chain); |
| |
| String algorithm = KeyManagerFactory.getDefaultAlgorithm(); |
| KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); |
| keyManagerFactory.init(keyStore, EMPTY_PASSWORD); |
| return keyManagerFactory.getKeyManagers(); |
| } |
| |
| ServerThread(CtsTestServer server, @SslMode int sslMode, InputStream key, |
| InputStream cert) throws Exception { |
| super("ServerThread"); |
| mServer = server; |
| mSsl = sslMode; |
| KeyManager[] keyManagers; |
| if (key == null && cert == null) { |
| keyManagers = getHardCodedKeyManagers(); |
| } else { |
| keyManagers = getKeyManagersFromStreams(key, cert); |
| } |
| int retry = 3; |
| while (true) { |
| try { |
| if (mSsl == SslMode.INSECURE) { |
| mSocket = new ServerSocket(0); |
| } else { // Use SSL |
| mSslContext = SSLContext.getInstance("TLS"); |
| mSslContext.init(keyManagers, mServer.getTrustManagers(), null); |
| mSocket = mSslContext.getServerSocketFactory().createServerSocket(0); |
| if (mSsl == SslMode.TRUST_ANY_CLIENT) { |
| HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { |
| @Override |
| public boolean verify(String s, SSLSession sslSession) { |
| return true; |
| } |
| }); |
| HttpsURLConnection.setDefaultSSLSocketFactory( |
| mSslContext.getSocketFactory()); |
| } else if (mSsl == SslMode.WANTS_CLIENT_AUTH) { |
| ((SSLServerSocket) mSocket).setWantClientAuth(true); |
| } else if (mSsl == SslMode.NEEDS_CLIENT_AUTH) { |
| ((SSLServerSocket) mSocket).setNeedClientAuth(true); |
| } |
| } |
| return; |
| } catch (IOException e) { |
| if (--retry == 0) { |
| throw e; |
| } |
| // sleep in case server socket is still being closed |
| Thread.sleep(1000); |
| } |
| } |
| } |
| |
| public void run() { |
| while (!mWillShutDown) { |
| try { |
| Socket socket = mSocket.accept(); |
| |
| synchronized(mLock) { |
| mSockets.add(socket); |
| } |
| |
| DefaultHttpServerConnection conn = mServer.createHttpServerConnection(); |
| HttpParams params = new BasicHttpParams(); |
| params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0); |
| conn.bind(socket, params); |
| |
| // Determine whether we need to shutdown early before |
| // parsing the response since conn.close() will crash |
| // for SSL requests due to UnsupportedOperationException. |
| HttpRequest request = conn.receiveRequestHeader(); |
| if (request instanceof HttpEntityEnclosingRequest) { |
| conn.receiveRequestEntity( (HttpEntityEnclosingRequest) request); |
| } |
| |
| mExecutorService.execute(new HandleResponseTask(conn, request, socket)); |
| } catch (IOException e) { |
| // normal during shutdown, ignore |
| Log.w(TAG, e); |
| } catch (RejectedExecutionException e) { |
| // normal during shutdown, ignore |
| Log.w(TAG, e); |
| } catch (HttpException e) { |
| Log.w(TAG, e); |
| } catch (UnsupportedOperationException e) { |
| // DefaultHttpServerConnection's close() throws an |
| // UnsupportedOperationException. |
| Log.w(TAG, e); |
| } |
| } |
| } |
| |
| /** |
| * Shutdown the socket and the executor service. |
| * Note this method is called on the client thread, instead of the server thread. |
| */ |
| public void shutDownOnClientThread() { |
| try { |
| mWillShutDown = true; |
| mExecutorService.shutdown(); |
| mExecutorService.awaitTermination(1L, TimeUnit.MINUTES); |
| mSocket.close(); |
| // To prevent the server thread from being blocked on read from socket, |
| // which is called when the server tries to receiveRequestHeader, |
| // close all the sockets here. |
| synchronized(mLock) { |
| for (Socket socket : mSockets) { |
| socket.close(); |
| } |
| } |
| } catch (IOException ignored) { |
| // safe to ignore |
| } catch (InterruptedException e) { |
| Log.e(TAG, "Shutting down threads", e); |
| } |
| } |
| |
| private class HandleResponseTask implements Runnable { |
| |
| private DefaultHttpServerConnection mConnection; |
| |
| private HttpRequest mRequest; |
| |
| private Socket mSocket; |
| |
| public HandleResponseTask(DefaultHttpServerConnection connection, |
| HttpRequest request, Socket socket) { |
| this.mConnection = connection; |
| this.mRequest = request; |
| this.mSocket = socket; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| HttpResponse response = mServer.getResponse(mRequest); |
| mConnection.sendResponseHeader(response); |
| mConnection.sendResponseEntity(response); |
| } catch (Exception e) { |
| Log.e(TAG, "Error handling request:", e); |
| } finally { |
| try { |
| mConnection.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Failed to close http connection", e); |
| } |
| |
| // mConnection.close() closes mSocket. |
| // mConnection only throws an IOException when the socket.close() call fails, at |
| // which point, there is not much that can be done anyways. |
| synchronized(mLock) { |
| ServerThread.this.mSockets.remove(mSocket); |
| } |
| } |
| } |
| } |
| } |
| } |