| 1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | package org.chromium.net.test.util; |
| 6 | |
| 7 | import android.util.Base64; |
| 8 | import android.util.Log; |
| 9 | import android.util.Pair; |
| 10 | |
| 11 | import org.apache.http.HttpException; |
| 12 | import org.apache.http.HttpRequest; |
| 13 | import org.apache.http.HttpResponse; |
| 14 | import org.apache.http.HttpStatus; |
| 15 | import org.apache.http.HttpVersion; |
| 16 | import org.apache.http.RequestLine; |
| 17 | import org.apache.http.StatusLine; |
| 18 | import org.apache.http.entity.ByteArrayEntity; |
| 19 | import org.apache.http.impl.DefaultHttpServerConnection; |
| 20 | import org.apache.http.impl.cookie.DateUtils; |
| 21 | import org.apache.http.message.BasicHttpResponse; |
| 22 | import org.apache.http.params.BasicHttpParams; |
| 23 | import org.apache.http.params.CoreProtocolPNames; |
| 24 | import org.apache.http.params.HttpParams; |
| 25 | |
| 26 | import java.io.ByteArrayInputStream; |
| 27 | import java.io.IOException; |
| 28 | import java.io.InputStream; |
| 29 | import java.net.MalformedURLException; |
| 30 | import java.net.ServerSocket; |
| 31 | import java.net.Socket; |
| 32 | import java.net.URI; |
| 33 | import java.net.URL; |
| 34 | import java.net.URLConnection; |
| 35 | import java.security.KeyManagementException; |
| 36 | import java.security.KeyStore; |
| 37 | import java.security.NoSuchAlgorithmException; |
| 38 | import java.security.cert.X509Certificate; |
| 39 | import java.util.ArrayList; |
| 40 | import java.util.Date; |
| 41 | import java.util.HashMap; |
| 42 | import java.util.Hashtable; |
| 43 | import java.util.List; |
| 44 | import java.util.Map; |
| 45 | |
| 46 | import javax.net.ssl.HostnameVerifier; |
| 47 | import javax.net.ssl.HttpsURLConnection; |
| 48 | import javax.net.ssl.KeyManager; |
| 49 | import javax.net.ssl.KeyManagerFactory; |
| 50 | import javax.net.ssl.SSLContext; |
| 51 | import javax.net.ssl.SSLSession; |
| 52 | import javax.net.ssl.X509TrustManager; |
| 53 | |
| 54 | /** |
| 55 | * Simple http test server for testing. |
| 56 | * |
| 57 | * This server runs in a thread in the current process, so it is convenient |
| 58 | * for loopback testing without the need to setup tcp forwarding to the |
| 59 | * host computer. |
| 60 | * |
| 61 | * Based heavily on the CTSWebServer in Android. |
| 62 | */ |
| 63 | public class TestWebServer { |
| 64 | private static final String TAG = "TestWebServer"; |
| 65 | private static final int SERVER_PORT = 4444; |
| 66 | private static final int SSL_SERVER_PORT = 4445; |
| 67 | |
| 68 | public static final String SHUTDOWN_PREFIX = "/shutdown"; |
| 69 | |
| 70 | private static TestWebServer sInstance; |
| 71 | private static Hashtable<Integer, String> sReasons; |
| 72 | |
| 73 | private final ServerThread mServerThread; |
| 74 | private String mServerUri; |
| 75 | private final boolean mSsl; |
| 76 | |
| 77 | private static class Response { |
| 78 | final byte[] mResponseData; |
| 79 | final List<Pair<String, String>> mResponseHeaders; |
| 80 | final boolean mIsRedirect; |
| 81 | |
| 82 | Response(byte[] resposneData, List<Pair<String, String>> responseHeaders, |
| 83 | boolean isRedirect) { |
| 84 | mIsRedirect = isRedirect; |
| 85 | mResponseData = resposneData; |
| 86 | mResponseHeaders = responseHeaders == null ? |
| 87 | new ArrayList<Pair<String, String>>() : responseHeaders; |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // The Maps below are modified on both the client thread and the internal server thread, so |
| 92 | // need to use a lock when accessing them. |
| 93 | private final Object mLock = new Object(); |
| 94 | private final Map<String, Response> mResponseMap = new HashMap<String, Response>(); |
| 95 | private final Map<String, Integer> mResponseCountMap = new HashMap<String, Integer>(); |
| 96 | private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>(); |
| 97 | |
| 98 | /** |
| 99 | * Create and start a local HTTP server instance. |
| 100 | * @param ssl True if the server should be using secure sockets. |
| 101 | * @throws Exception |
| 102 | */ |
| 103 | public TestWebServer(boolean ssl) throws Exception { |
| 104 | if (sInstance != null) { |
| 105 | // attempt to start a new instance while one is still running |
| 106 | // shut down the old instance first |
| 107 | sInstance.shutdown(); |
| 108 | } |
| 109 | setStaticInstance(this); |
| 110 | mSsl = ssl; |
| 111 | if (mSsl) { |
| 112 | mServerUri = "https://localhost:" + SSL_SERVER_PORT; |
| 113 | } else { |
| 114 | mServerUri = "http://localhost:" + SERVER_PORT; |
| 115 | } |
| 116 | mServerThread = new ServerThread(this, mSsl); |
| 117 | mServerThread.start(); |
| 118 | } |
| 119 | |
| 120 | private static void setStaticInstance(TestWebServer instance) { |
| 121 | sInstance = instance; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Terminate the http server. |
| 126 | */ |
| 127 | public void shutdown() { |
| 128 | try { |
| 129 | // Avoid a deadlock between two threads where one is trying to call |
| 130 | // close() and the other one is calling accept() by sending a GET |
| 131 | // request for shutdown and having the server's one thread |
| 132 | // sequentially call accept() and close(). |
| 133 | URL url = new URL(mServerUri + SHUTDOWN_PREFIX); |
| 134 | URLConnection connection = openConnection(url); |
| 135 | connection.connect(); |
| 136 | |
| 137 | // Read the input from the stream to send the request. |
| 138 | InputStream is = connection.getInputStream(); |
| 139 | is.close(); |
| 140 | |
| 141 | // Block until the server thread is done shutting down. |
| 142 | mServerThread.join(); |
| 143 | |
| 144 | } catch (MalformedURLException e) { |
| 145 | throw new IllegalStateException(e); |
| 146 | } catch (InterruptedException e) { |
| 147 | throw new RuntimeException(e); |
| 148 | } catch (IOException e) { |
| 149 | throw new RuntimeException(e); |
| 150 | } catch (NoSuchAlgorithmException e) { |
| 151 | throw new IllegalStateException(e); |
| 152 | } catch (KeyManagementException e) { |
| 153 | throw new IllegalStateException(e); |
| 154 | } |
| 155 | |
| 156 | setStaticInstance(null); |
| 157 | } |
| 158 | |
| 159 | private final static int RESPONSE_STATUS_NORMAL = 0; |
| 160 | private final static int RESPONSE_STATUS_MOVED_TEMPORARILY = 1; |
| 161 | |
| 162 | private String setResponseInternal( |
| 163 | String requestPath, byte[] responseData, |
| 164 | List<Pair<String, String>> responseHeaders, |
| 165 | int status) { |
| 166 | final boolean isRedirect = (status == RESPONSE_STATUS_MOVED_TEMPORARILY); |
| 167 | |
| 168 | synchronized (mLock) { |
| 169 | mResponseMap.put(requestPath, new Response(responseData, responseHeaders, isRedirect)); |
| 170 | mResponseCountMap.put(requestPath, Integer.valueOf(0)); |
| 171 | mLastRequestMap.put(requestPath, null); |
| 172 | } |
| 173 | return getResponseUrl(requestPath); |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Gets the URL on the server under which a particular request path will be accessible. |
| 178 | * |
| 179 | * This only gets the URL, you still need to set the response if you intend to access it. |
| 180 | * |
| 181 | * @param requestPath The path to respond to. |
| 182 | * @return The full URL including the requestPath. |
| 183 | */ |
| 184 | public String getResponseUrl(String requestPath) { |
| 185 | return mServerUri + requestPath; |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Sets a response to be returned when a particular request path is passed |
| 190 | * in (with the option to specify additional headers). |
| 191 | * |
| 192 | * @param requestPath The path to respond to. |
| 193 | * @param responseString The response body that will be returned. |
| 194 | * @param responseHeaders Any additional headers that should be returned along with the |
| 195 | * response (null is acceptable). |
| 196 | * @return The full URL including the path that should be requested to get the expected |
| 197 | * response. |
| 198 | */ |
| 199 | public String setResponse( |
| 200 | String requestPath, String responseString, |
| 201 | List<Pair<String, String>> responseHeaders) { |
| 202 | return setResponseInternal(requestPath, responseString.getBytes(), responseHeaders, |
| 203 | RESPONSE_STATUS_NORMAL); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Sets a redirect. |
| 208 | * |
| 209 | * @param requestPath The path to respond to. |
| 210 | * @param targetPath The path to redirect to. |
| 211 | * @return The full URL including the path that should be requested to get the expected |
| 212 | * response. |
| 213 | */ |
| 214 | public String setRedirect( |
| 215 | String requestPath, String targetPath) { |
| 216 | List<Pair<String, String>> responseHeaders = new ArrayList<Pair<String, String>>(); |
| 217 | responseHeaders.add(Pair.create("Location", targetPath)); |
| 218 | |
| 219 | return setResponseInternal(requestPath, targetPath.getBytes(), responseHeaders, |
| 220 | RESPONSE_STATUS_MOVED_TEMPORARILY); |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Sets a base64 encoded response to be returned when a particular request path is passed |
| 225 | * in (with the option to specify additional headers). |
| 226 | * |
| 227 | * @param requestPath The path to respond to. |
| 228 | * @param base64EncodedResponse The response body that is base64 encoded. The actual server |
| 229 | * response will the decoded binary form. |
| 230 | * @param responseHeaders Any additional headers that should be returned along with the |
| 231 | * response (null is acceptable). |
| 232 | * @return The full URL including the path that should be requested to get the expected |
| 233 | * response. |
| 234 | */ |
| 235 | public String setResponseBase64( |
| 236 | String requestPath, String base64EncodedResponse, |
| 237 | List<Pair<String, String>> responseHeaders) { |
| 238 | return setResponseInternal(requestPath, |
| 239 | Base64.decode(base64EncodedResponse, Base64.DEFAULT), |
| 240 | responseHeaders, |
| 241 | RESPONSE_STATUS_NORMAL); |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * Get the number of requests was made at this path since it was last set. |
| 246 | */ |
| 247 | public int getRequestCount(String requestPath) { |
| 248 | Integer count = null; |
| 249 | synchronized (mLock) { |
| 250 | count = mResponseCountMap.get(requestPath); |
| 251 | } |
| 252 | if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath); |
| 253 | return count.intValue(); |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Returns the last HttpRequest at this path. Can return null if it is never requested. |
| 258 | */ |
| 259 | public HttpRequest getLastRequest(String requestPath) { |
| 260 | synchronized (mLock) { |
| 261 | if (!mLastRequestMap.containsKey(requestPath)) |
| 262 | throw new IllegalArgumentException("Path not set: " + requestPath); |
| 263 | return mLastRequestMap.get(requestPath); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | public String getBaseUrl() { |
| 268 | return mServerUri + "/"; |
| 269 | } |
| 270 | |
| 271 | private URLConnection openConnection(URL url) |
| 272 | throws IOException, NoSuchAlgorithmException, KeyManagementException { |
| 273 | if (mSsl) { |
| 274 | // Install hostname verifiers and trust managers that don't do |
| 275 | // anything in order to get around the client not trusting |
| 276 | // the test server due to a lack of certificates. |
| 277 | |
| 278 | HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); |
| 279 | connection.setHostnameVerifier(new TestHostnameVerifier()); |
| 280 | |
| 281 | SSLContext context = SSLContext.getInstance("TLS"); |
| 282 | TestTrustManager trustManager = new TestTrustManager(); |
| 283 | context.init(null, new TestTrustManager[] {trustManager}, null); |
| 284 | connection.setSSLSocketFactory(context.getSocketFactory()); |
| 285 | |
| 286 | return connection; |
| 287 | } else { |
| 288 | return url.openConnection(); |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * {@link X509TrustManager} that trusts everybody. This is used so that |
| 294 | * the client calling {@link TestWebServer#shutdown()} can issue a request |
| 295 | * for shutdown by blindly trusting the {@link TestWebServer}'s |
| 296 | * credentials. |
| 297 | */ |
| 298 | private static class TestTrustManager implements X509TrustManager { |
| 299 | @Override |
| 300 | public void checkClientTrusted(X509Certificate[] chain, String authType) { |
| 301 | // Trust the TestWebServer... |
| 302 | } |
| 303 | |
| 304 | @Override |
| 305 | public void checkServerTrusted(X509Certificate[] chain, String authType) { |
| 306 | // Trust the TestWebServer... |
| 307 | } |
| 308 | |
| 309 | @Override |
| 310 | public X509Certificate[] getAcceptedIssuers() { |
| 311 | return null; |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | /** |
| 316 | * {@link HostnameVerifier} that verifies everybody. This permits |
| 317 | * the client to trust the web server and call |
| 318 | * {@link TestWebServer#shutdown()}. |
| 319 | */ |
| 320 | private static class TestHostnameVerifier implements HostnameVerifier { |
| 321 | @Override |
| 322 | public boolean verify(String hostname, SSLSession session) { |
| 323 | return true; |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | private void servedResponseFor(String path, HttpRequest request) { |
| 328 | synchronized (mLock) { |
| 329 | mResponseCountMap.put(path, Integer.valueOf( |
| 330 | mResponseCountMap.get(path).intValue() + 1)); |
| 331 | mLastRequestMap.put(path, request); |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | /** |
| 336 | * Generate a response to the given request. |
| 337 | * @throws InterruptedException |
| 338 | */ |
| 339 | private HttpResponse getResponse(HttpRequest request) throws InterruptedException { |
| 340 | RequestLine requestLine = request.getRequestLine(); |
| 341 | HttpResponse httpResponse = null; |
| 342 | Log.i(TAG, requestLine.getMethod() + ": " + requestLine.getUri()); |
| 343 | String uriString = requestLine.getUri(); |
| 344 | URI uri = URI.create(uriString); |
| 345 | String path = uri.getPath(); |
| 346 | |
| 347 | Response response = null; |
| 348 | synchronized (mLock) { |
| 349 | response = mResponseMap.get(path); |
| 350 | } |
| 351 | if (path.equals(SHUTDOWN_PREFIX)) { |
| 352 | httpResponse = createResponse(HttpStatus.SC_OK); |
| 353 | } else if (response == null) { |
| 354 | httpResponse = createResponse(HttpStatus.SC_NOT_FOUND); |
| 355 | } else if (response.mIsRedirect) { |
| 356 | httpResponse = createResponse(HttpStatus.SC_MOVED_TEMPORARILY); |
| 357 | for (Pair<String, String> header : response.mResponseHeaders) { |
| 358 | httpResponse.addHeader(header.first, header.second); |
| 359 | } |
| 360 | servedResponseFor(path, request); |
| 361 | } else { |
| 362 | httpResponse = createResponse(HttpStatus.SC_OK); |
| 363 | httpResponse.setEntity(createEntity(response.mResponseData)); |
| 364 | for (Pair<String, String> header : response.mResponseHeaders) { |
| 365 | httpResponse.addHeader(header.first, header.second); |
| 366 | } |
| 367 | servedResponseFor(path, request); |
| 368 | } |
| 369 | StatusLine sl = httpResponse.getStatusLine(); |
| 370 | Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")"); |
| 371 | setDateHeaders(httpResponse); |
| 372 | return httpResponse; |
| 373 | } |
| 374 | |
| 375 | private void setDateHeaders(HttpResponse response) { |
| 376 | response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123)); |
| 377 | } |
| 378 | |
| 379 | /** |
| 380 | * Create an empty response with the given status. |
| 381 | */ |
| 382 | private HttpResponse createResponse(int status) { |
| 383 | HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null); |
| 384 | String reason = null; |
| 385 | |
| 386 | // This synchronized silences findbugs. |
| 387 | synchronized (TestWebServer.class) { |
| 388 | if (sReasons == null) { |
| 389 | sReasons = new Hashtable<Integer, String>(); |
| 390 | sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized"); |
| 391 | sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found"); |
| 392 | sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden"); |
| 393 | sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily"); |
| 394 | } |
| 395 | // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is |
| 396 | // Locale-dependent. |
| 397 | reason = sReasons.get(status); |
| 398 | } |
| 399 | |
| 400 | if (reason != null) { |
| 401 | StringBuffer buf = new StringBuffer("<html><head><title>"); |
| 402 | buf.append(reason); |
| 403 | buf.append("</title></head><body>"); |
| 404 | buf.append(reason); |
| 405 | buf.append("</body></html>"); |
| 406 | response.setEntity(createEntity(buf.toString().getBytes())); |
| 407 | } |
| 408 | return response; |
| 409 | } |
| 410 | |
| 411 | /** |
| 412 | * Create a string entity for the given content. |
| 413 | */ |
| 414 | private ByteArrayEntity createEntity(byte[] data) { |
| 415 | ByteArrayEntity entity = new ByteArrayEntity(data); |
| 416 | entity.setContentType("text/html"); |
| 417 | return entity; |
| 418 | } |
| 419 | |
| 420 | private static class ServerThread extends Thread { |
| 421 | private TestWebServer mServer; |
| 422 | private ServerSocket mSocket; |
| 423 | private boolean mIsSsl; |
| 424 | private boolean mIsCancelled; |
| 425 | private SSLContext mSslContext; |
| 426 | |
| 427 | /** |
| 428 | * Defines the keystore contents for the server, BKS version. Holds just a |
| 429 | * single self-generated key. The subject name is "Test Server". |
| 430 | */ |
| 431 | private static final String SERVER_KEYS_BKS = |
| 432 | "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" + |
| 433 | "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" + |
| 434 | "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" + |
| 435 | "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" + |
| 436 | "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" + |
| 437 | "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" + |
| 438 | "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" + |
| 439 | "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" + |
| 440 | "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" + |
| 441 | "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" + |
| 442 | "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" + |
| 443 | "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" + |
| 444 | "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" + |
| 445 | "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" + |
| 446 | "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" + |
| 447 | "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" + |
| 448 | "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" + |
| 449 | "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" + |
| 450 | "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" + |
| 451 | "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" + |
| 452 | "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" + |
| 453 | "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" + |
| 454 | "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" + |
| 455 | "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw="; |
| 456 | |
| 457 | private static final String PASSWORD = "android"; |
| 458 | |
| 459 | /** |
| 460 | * Loads a keystore from a base64-encoded String. Returns the KeyManager[] |
| 461 | * for the result. |
| 462 | */ |
| 463 | private KeyManager[] getKeyManagers() throws Exception { |
| 464 | byte[] bytes = Base64.decode(SERVER_KEYS_BKS, Base64.DEFAULT); |
| 465 | InputStream inputStream = new ByteArrayInputStream(bytes); |
| 466 | |
| 467 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| 468 | keyStore.load(inputStream, PASSWORD.toCharArray()); |
| 469 | inputStream.close(); |
| 470 | |
| 471 | String algorithm = KeyManagerFactory.getDefaultAlgorithm(); |
| 472 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); |
| 473 | keyManagerFactory.init(keyStore, PASSWORD.toCharArray()); |
| 474 | |
| 475 | return keyManagerFactory.getKeyManagers(); |
| 476 | } |
| 477 | |
| 478 | |
| 479 | public ServerThread(TestWebServer server, boolean ssl) throws Exception { |
| 480 | super("ServerThread"); |
| 481 | mServer = server; |
| 482 | mIsSsl = ssl; |
| 483 | int retry = 3; |
| 484 | while (true) { |
| 485 | try { |
| 486 | if (mIsSsl) { |
| 487 | mSslContext = SSLContext.getInstance("TLS"); |
| 488 | mSslContext.init(getKeyManagers(), null, null); |
| 489 | mSocket = mSslContext.getServerSocketFactory().createServerSocket( |
| 490 | SSL_SERVER_PORT); |
| 491 | } else { |
| 492 | mSocket = new ServerSocket(SERVER_PORT); |
| 493 | } |
| 494 | return; |
| 495 | } catch (IOException e) { |
| 496 | Log.w(TAG, e); |
| 497 | if (--retry == 0) { |
| 498 | throw e; |
| 499 | } |
| 500 | // sleep in case server socket is still being closed |
| 501 | Thread.sleep(1000); |
| 502 | } |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | @Override |
| 507 | public void run() { |
| 508 | HttpParams params = new BasicHttpParams(); |
| 509 | params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0); |
| 510 | while (!mIsCancelled) { |
| 511 | try { |
| 512 | Socket socket = mSocket.accept(); |
| 513 | DefaultHttpServerConnection conn = new DefaultHttpServerConnection(); |
| 514 | conn.bind(socket, params); |
| 515 | |
| 516 | // Determine whether we need to shutdown early before |
| 517 | // parsing the response since conn.close() will crash |
| 518 | // for SSL requests due to UnsupportedOperationException. |
| 519 | HttpRequest request = conn.receiveRequestHeader(); |
| 520 | if (isShutdownRequest(request)) { |
| 521 | mIsCancelled = true; |
| 522 | } |
| 523 | |
| 524 | HttpResponse response = mServer.getResponse(request); |
| 525 | conn.sendResponseHeader(response); |
| 526 | conn.sendResponseEntity(response); |
| 527 | conn.close(); |
| 528 | |
| 529 | } catch (IOException e) { |
| 530 | // normal during shutdown, ignore |
| 531 | Log.w(TAG, e); |
| 532 | } catch (HttpException e) { |
| 533 | Log.w(TAG, e); |
| 534 | } catch (InterruptedException e) { |
| 535 | Log.w(TAG, e); |
| 536 | } catch (UnsupportedOperationException e) { |
| 537 | // DefaultHttpServerConnection's close() throws an |
| 538 | // UnsupportedOperationException. |
| 539 | Log.w(TAG, e); |
| 540 | } |
| 541 | } |
| 542 | try { |
| 543 | mSocket.close(); |
| 544 | } catch (IOException ignored) { |
| 545 | // safe to ignore |
| 546 | } |
| 547 | } |
| 548 | |
| 549 | private boolean isShutdownRequest(HttpRequest request) { |
| 550 | RequestLine requestLine = request.getRequestLine(); |
| 551 | String uriString = requestLine.getUri(); |
| 552 | URI uri = URI.create(uriString); |
| 553 | String path = uri.getPath(); |
| 554 | return path.equals(SHUTDOWN_PREFIX); |
| 555 | } |
| 556 | } |
| 557 | } |