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