package com.netflix.client.testutil; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.security.KeyStore; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManagerFactory; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.sun.jersey.core.util.Base64; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsParameters; import com.sun.net.httpserver.HttpsServer; /** * Rule for running an embedded HTTP server for basic testing. The * server uses ephemeral ports to ensure there are no conflicts when * running concurrent unit tests * * The server uses HttpServer classes available in the JVM to avoid * pulling in any extra dependencies. * * Available endpoints are, * * / Returns a 200 * /status?code=${code} Returns a request provide code * /noresponse No response from the server * * Optional query parameters * delay=${delay} Inject a delay into the request * * @author elandau * */ public class MockHttpServer implements TestRule { public static final String TEST_TS1 = "/u3+7QAAAAIAAAABAAAAAgALcmliYm9uX3Jvb3QAAAFA9E6KkQAFWC41MDkAAAIyMIICLjCCAZeg" + "AwIBAgIBATANBgkqhkiG9w0BAQUFADBTMRgwFgYDVQQDDA9SaWJib25UZXN0Um9vdDExCzAJBgNV" + "BAsMAklUMRAwDgYDVQQKDAdOZXRmbGl4MQswCQYDVQQIDAJDQTELMAkGA1UEBhMCVVMwIBcNMTMw" + "OTA2MTcyNTIyWhgPMjExMzA4MTMxNzI1MjJaMFMxGDAWBgNVBAMMD1JpYmJvblRlc3RSb290MTEL" + "MAkGA1UECwwCSVQxEDAOBgNVBAoMB05ldGZsaXgxCzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzCB" + "nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAg8riOgT2Y39SQlZE+MWnOiKjREZzQ3ecvPf40oF8" + "9YPNGpBhJzIKdA0TR1vQ70p3Fl2+Y5txs1H2/iguOdFMBrSdv1H8qJG1UufaeYO++HBm3Mi2L02F" + "6fcTEEyXQMebKCWf04mxvLy5M6B5yMqZ9rHEZD+qsF4rXspx70bd0tUCAwEAAaMQMA4wDAYDVR0T" + "BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBzTEn9AZniODYSRa+N7IvZu127rh+Sc6XWth68TBRj" + "hThDFARnGxxe2d3EFXB4xH7qcvLl3HQ3U6lIycyLabdm06D3/jzu68mkMToE5sHJmrYNHHTVl0aj" + "0gKFBQjLRJRlgJ3myUbbfrM+/a5g6S90TsVGTxXwFn5bDvdErsn8F8Hd41plMkW5ywsn6yFZMaFr" + "MxnX"; // Keystore type: JKS // Keystore provider: SUN // // Your keystore contains 1 entry // // Alias name: ribbon_root // Creation date: Sep 6, 2013 // Entry type: trustedCertEntry // // Owner: C=US, ST=CA, O=Netflix, OU=IT, CN=RibbonTestRoot2 // Issuer: C=US, ST=CA, O=Netflix, OU=IT, CN=RibbonTestRoot2 // Serial number: 1 // Valid from: Fri Sep 06 10:26:22 PDT 2013 until: Sun Aug 13 10:26:22 PDT 2113 // Certificate fingerprints: // MD5: 44:64:E3:25:4F:D2:2C:8D:4D:B0:53:19:59:BD:B3:20 // SHA1: 26:2F:41:6D:03:C7:D0:8E:4F:AF:0E:4F:29:E3:08:53:B7:3C:DB:EE // Signature algorithm name: SHA1withRSA // Version: 3 // // Extensions: // // #1: ObjectId: 2.5.29.19 Criticality=false // BasicConstraints:[ // CA:true // PathLen:2147483647 // ] public static final String TEST_TS2 = "/u3+7QAAAAIAAAABAAAAAgALcmliYm9uX3Jvb3QAAAFA9E92vgAFWC41MDkAAAIyMIICLjCCAZeg" + "AwIBAgIBATANBgkqhkiG9w0BAQUFADBTMRgwFgYDVQQDDA9SaWJib25UZXN0Um9vdDIxCzAJBgNV" + "BAsMAklUMRAwDgYDVQQKDAdOZXRmbGl4MQswCQYDVQQIDAJDQTELMAkGA1UEBhMCVVMwIBcNMTMw" + "OTA2MTcyNjIyWhgPMjExMzA4MTMxNzI2MjJaMFMxGDAWBgNVBAMMD1JpYmJvblRlc3RSb290MjEL" + "MAkGA1UECwwCSVQxEDAOBgNVBAoMB05ldGZsaXgxCzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzCB" + "nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAnEwAfuHYKRJviVB3RyV3+/mp4qjWZZd/q+fE2Z0k" + "o2N2rrC8fAw53KXwGOE5fED6wXd3B2zyoSFHVsWOeL+TUoohn+eHSfwH7xK+0oWC8IvUoXWehOft" + "grYtv9Jt5qNY5SmspBmyxFiaiAWQJYuf12Ycu4Gqg+P7mieMHgu6Do0CAwEAAaMQMA4wDAYDVR0T" + "BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBNA0ask9eTYYhYA3bbmQZInxkBV74Gq/xorLlVygjn" + "OgyGYp4/L274qwlPMqnQRmVbezkug2YlUK8xbrjwCUvHq2XW38e2RjK5q3EXVkGJxgCBuHug/eIf" + "wD+/IEIE8aVkTW2j1QrrdkXDhRO5OsjvIVdy5/V4U0hVDnSo865ud9VQ/hZmOQuZItHViSoGSe2j" + "bbZk"; // Keystore type: JKS // Keystore provider: SUN // // Your keystore contains 1 entry // // Alias name: ribbon_key // Creation date: Sep 6, 2013 // Entry type: PrivateKeyEntry // Certificate chain length: 1 // Certificate[1]: // Owner: C=US, ST=CA, O=Netflix, OU=IT, CN=RibbonTestEndEntity1 // Issuer: C=US, ST=CA, O=Netflix, OU=IT, CN=RibbonTestRoot1 // Serial number: 64 // Valid from: Fri Sep 06 10:25:22 PDT 2013 until: Sun Aug 13 10:25:22 PDT 2113 // Certificate fingerprints: // MD5: 79:C5:F3:B2:B8:4C:F2:3F:2E:C7:67:FC:E7:04:BF:90 // SHA1: B1:D0:4E:A6:D8:84:BF:8B:01:46:B6:EA:97:5B:A0:4E:13:8B:A9:DE // Signature algorithm name: SHA1withRSA // Version: 3 public static final String TEST_KS1 = "/u3+7QAAAAIAAAABAAAAAQAKcmliYm9uX2tleQAAAUD0Toq4AAACuzCCArcwDgYKKwYBBAEqAhEB" + "AQUABIICo9fLN8zeXLcyx50+6B6gdXiUslZKY0SgLwL8kKNlQFiccD3Oc1yWjMnVC2QOdLsFzcVk" + "ROhgMH/nHfFeXFlvY5IYMXqhbEC37LjE52RtX5KHv4FLYxxZCHduAwO8UTPa603XzrJ0VTMJ6Hso" + "9+Ql76cGxPtIPcYm8IfqIY22as3NlKO4eMbiur9GLvuC57eql8vROaxGy8y657gc6kZMUyQOC+HG" + "a5M3DTFpjl4V6HHbXHhMNEk9eXHnrZwYVOJmOgdgIrNNHOyD4kE+k21C7rUHhLAwK84wKL/tW4k9" + "xnhOJK/L1RmycRIFWwXVi3u/3vi49bzdZsRLn73MdQkTe5p8oNZzG9sxg76u67ua6+99TMZYE1ay" + "5JCYgbr85KbRsoX9Hd5XBcSNzROKJl0To2tAF8eTTMRlhEy7JZyTF2M9877juNaregVwE3Tp+a/J" + "ACeNMyrxOQItNDam7a5dgBohpM8oJdEFqqj/S9BU7H5sR0XYo8TyIe1BV9zR5ZC/23fj5l5zkrri" + "TCMgMbvt95JUGOT0gSzxBMmhV+ZLxpmVz3M5P2pXX0DXGTKfuHSiBWrh1GAQL4BOVpuKtyXlH1/9" + "55/xY25W0fpLzMiQJV7jf6W69LU0FAFWFH9uuwf/sFph0S1QQXcQSfpYmWPMi1gx/IgIbvT1xSuI" + "6vajgFqv6ctiVbFAJ6zmcnGd6e33+Ao9pmjs5JPZP3rtAYd6+PxtlwUbGLZuqIVK4o68LEISDfvm" + "nGlk4/1+S5CILKVqTC6Ja8ojwUjjsNSJbZwHue3pOkmJQUNtuK6kDOYXgiMRLURbrYLyen0azWw8" + "C5/nPs5J4pN+irD/hhD6cupCnUJmzMw30u8+LOCN6GaM5fdCTQ2uQKF7quYuD+gR3lLNOqq7KAAA" + "AAEABVguNTA5AAACJTCCAiEwggGKoAMCAQICAWQwDQYJKoZIhvcNAQEFBQAwUzEYMBYGA1UEAwwP" + "UmliYm9uVGVzdFJvb3QxMQswCQYDVQQLDAJJVDEQMA4GA1UECgwHTmV0ZmxpeDELMAkGA1UECAwC" + "Q0ExCzAJBgNVBAYTAlVTMCAXDTEzMDkwNjE3MjUyMloYDzIxMTMwODEzMTcyNTIyWjBYMR0wGwYD" + "VQQDDBRSaWJib25UZXN0RW5kRW50aXR5MTELMAkGA1UECwwCSVQxEDAOBgNVBAoMB05ldGZsaXgx" + "CzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAydqk" + "AJuUcSF8dpUbNJWl+G1usgHtEFEbOMm54N/ZqGC7iSYs6EXfeoEyiHrMw/hdCKACERq2vuiuqan8" + "h6z65/DXIiHUyykGb/Z4NK1I0aCQLZG4Ek3sERilILWyy2NRpjUrvqDPr/mQgymXqpuYhSD81jHx" + "F84AOpTrnGsY7/sCAwEAATANBgkqhkiG9w0BAQUFAAOBgQAjvtRKNhb1R6XIuWaOxJ0XDLine464" + "Ie7LDfkE/KB43oE4MswjRh7nR9q6C73oa6TlIXmW6ysyKPp0vAyWHlq/zZhL3gNQ6faHuYHqas5s" + "nJQgvQpHAQh4VXRyZt1K8ZdsHg3Qbd4APTL0aRVQkxDt+Dxd6AsoRMKmO/c5CRwUFIV/CK7k5VSh" + "Sl5PRtH3PVj2vp84"; // Keystore type: JKS // Keystore provider: SUN // // Your keystore contains 1 entry // // Alias name: ribbon_key // Creation date: Sep 6, 2013 // Entry type: PrivateKeyEntry // Certificate chain length: 1 // Certificate[1]: // Owner: C=US, ST=CA, O=Netflix, OU=IT, CN=RibbonTestEndEntity2 // Issuer: C=US, ST=CA, O=Netflix, OU=IT, CN=RibbonTestRoot2 // Serial number: 64 // Valid from: Fri Sep 06 10:26:22 PDT 2013 until: Sun Aug 13 10:26:22 PDT 2113 // Certificate fingerprints: // MD5: C3:AF:6A:DB:18:ED:70:22:83:73:9A:A5:DB:58:6D:04 // SHA1: B7:D4:F0:87:A8:4E:49:0A:91:B1:7B:62:28:CA:A2:4A:0E:AE:40:CC // Signature algorithm name: SHA1withRSA // Version: 3 // // // public static final String TEST_KS2 = "/u3+7QAAAAIAAAABAAAAAQAKcmliYm9uX2tleQAAAUD0T3bNAAACuzCCArcwDgYKKwYBBAEqAhEB" + "AQUABIICoysDP4inwmxxKZkS8EYMW3DCJCD1AmpwFHxJIzo2v9fMysg+vjKxsrvVyKG23ZcHcznI" + "ftmrEpriCCUZp+NNAf0EJWVAIzGenwrsd0+rI5I96gBOh9slJUzgucn7164R3XKNKk+VWcwGJRh+" + "IuHxVrwFN025pfhlBJXNGJg4ZlzB7ZwcQPYblBzhLbhS3vJ1Vc46pEYWpnjxmHaDSetaQIcueAp8" + "HnTUkFMXJ6t51N0u9QMPhBH7p7N8tNjaa5noSdxhSl/2Znj6r04NwQU1qX2n4rSWEnYaW1qOVkgx" + "YrQzxI2kSHZfQDM2T918UikboQvAS1aX4h5P3gVCDKLr3EOO6UYO0ZgLHUr/DZrhVKd1KAhnzaJ8" + "BABxot2ES7Zu5EzY9goiaYDA2/bkURmt0zDdKpeORb7r59XBZUm/8D80naaNnE45W/gBA9bCiDu3" + "R99xie447c7ZX9Jio25yil3ncv+npBO1ozc5QIgQnbEfxbbwii3//shvPT6oxYPrcwWBXnaJNC5w" + "2HDpCTXJZNucyjnNVVxC7p1ANNnvvZhgC0+GpEqmf/BW+fb9Qu+AXe0/h4Vnoe/Zs92vPDehpaKy" + "oe+jBlUNiW2bpR88DSqxVcIu1DemlgzPa1Unzod0FdrOr/272bJnB2zAo4OBaBSv3KNf/rsMKjsU" + "X2Po77+S+PKoQkqd8KJFpmLEb0vxig9JsrTDJXLf4ebeSA1W7+mBotimMrp646PA3NciMSbS4csh" + "A7o/dBYhHlEosVgThm1JknIKhemf+FZsOzR3bJDT1oXJ/GhYpfzlCLyVFBeVP0KRnhih4xO0MEO7" + "Q21DBaTTqAvUo7Iv3/F3mGMOanLcLgoRoq3moQ7FhfCDRtRAPA1qT2+pxPG5wqlGeYc6McOvogAA" + "AAEABVguNTA5AAACJTCCAiEwggGKoAMCAQICAWQwDQYJKoZIhvcNAQEFBQAwUzEYMBYGA1UEAwwP" + "UmliYm9uVGVzdFJvb3QyMQswCQYDVQQLDAJJVDEQMA4GA1UECgwHTmV0ZmxpeDELMAkGA1UECAwC" + "Q0ExCzAJBgNVBAYTAlVTMCAXDTEzMDkwNjE3MjYyMloYDzIxMTMwODEzMTcyNjIyWjBYMR0wGwYD" + "VQQDDBRSaWJib25UZXN0RW5kRW50aXR5MjELMAkGA1UECwwCSVQxEDAOBgNVBAoMB05ldGZsaXgx" + "CzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtOvK" + "fBMC/oMo4xf4eYGN7hTNn+IywaSaVYndqECIfuznoIqRKSbeCKPSGs4CN1D+u3E2UoXcsDmTguHU" + "fokDA7sLUu8wD5ndAXfCkP3gXlFtUpNz/jPaXDsMFntTn2BdLKccxRxNwtwC0zzwIdtx9pw/Ru0g" + "NXIQnPi50aql5WcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCYWM2ZdBjG3jCvMw/RkebMLkEDRxVM" + "XU63Ygo+iCZUk8V8d0/S48j8Nk/hhVGHljsHqE/dByEF77X6uHaDGtt3Xwe+AYPofoJukh89jKnT" + "jEDtLF+y5AVfz6b2z3TnJcuigMr4ZtBFv18R00KLnVAznl/waXG8ix44IL5ss6nRZBJE4jr+ZMG9" + "9I4P1YhySxo3Qd3g"; public static final String PASSWORD = "changeit"; public static final String INTERNALERROR_PATH = "/internalerror"; public static final String NORESPONSE_PATH = "/noresponse"; public static final String STATUS_PATH = "/status"; public static final String OK_PATH = "/ok"; public static final String ROOT_PATH = "/"; private static final int DEFAULT_THREAD_COUNT = 10; private static final String DELAY_QUERY_PARAM = "delay"; private HttpServer server; private int localHttpServerPort = 0; private ExecutorService service; private int threadCount = DEFAULT_THREAD_COUNT; private LinkedHashMap<String, HttpHandler> handlers = new LinkedHashMap<String, HttpHandler>(); private boolean hasSsl = false; private File keystore; private File truststore; private String GENERIC_RESPONSE = "GenericTestHttpServer Response"; public MockHttpServer() { handlers.put(ROOT_PATH, new TestHttpHandler() { @Override protected void handle(RequestContext context) throws IOException { context.response(200, GENERIC_RESPONSE); }}); handlers.put(OK_PATH, new TestHttpHandler() { @Override protected void handle(RequestContext context) throws IOException { context.response(200, GENERIC_RESPONSE); }}); handlers.put(STATUS_PATH, new TestHttpHandler() { @Override protected void handle(RequestContext context) throws IOException { context.response(Integer.parseInt(context.query("code")), GENERIC_RESPONSE); }}); handlers.put(NORESPONSE_PATH, new TestHttpHandler() { @Override protected void handle(RequestContext context) throws IOException { }}); handlers.put(INTERNALERROR_PATH, new TestHttpHandler() { @Override protected void handle(RequestContext context) throws IOException { throw new RuntimeException("InternalError"); }}); } public MockHttpServer handler(String path, HttpHandler handler) { handlers.put(path, handler); return this; } public MockHttpServer port(int port) { this.localHttpServerPort = port; return this; } public MockHttpServer secure() { this.hasSsl = true; return this; } public MockHttpServer threadCount(int threads) { this.threadCount = threads; return this; } @Override public Statement apply(final Statement statement, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { before(description); try { statement.evaluate(); } finally { after(description); } } }; } private static interface RequestContext { void response(int code, String body) throws IOException; String query(String key); } private static abstract class TestHttpHandler implements HttpHandler { @Override public final void handle(final HttpExchange t) throws IOException { try { final Map<String, String> queryParameters = queryToMap(t); if (queryParameters.containsKey(DELAY_QUERY_PARAM)) { Long delay = Long.parseLong(queryParameters.get(DELAY_QUERY_PARAM)); if (delay != null) { try { TimeUnit.MILLISECONDS.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } handle(new RequestContext() { @Override public void response(int code, String body) throws IOException { OutputStream os = t.getResponseBody(); t.sendResponseHeaders(code, body.length()); os.write(body.getBytes()); os.close(); } @Override public String query(String key) { return queryParameters.get(key); } }); } catch (Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); String body = sw.toString(); OutputStream os = t.getResponseBody(); t.sendResponseHeaders(500, body.length()); os.write(body.getBytes()); os.close(); } } protected abstract void handle(RequestContext context) throws IOException; private static Map<String, String> queryToMap(HttpExchange t) { String queryString = t.getRequestURI().getQuery(); Map<String, String> result = new HashMap<String, String>(); if (queryString != null) { for (String param : queryString.split("&")) { String pair[] = param.split("="); if (pair.length>1) { result.put(pair[0], pair[1]); } else{ result.put(pair[0], ""); } } } return result; } } public void before(final Description description) throws Exception { this.service = Executors.newFixedThreadPool( threadCount, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("TestHttpServer-%d").build()); InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 0); if (hasSsl) { byte[] sampleTruststore1 = Base64.decode(TEST_TS1); byte[] sampleKeystore1 = Base64.decode(TEST_KS1); keystore = File.createTempFile("SecureAcceptAllGetTest", ".keystore"); truststore = File.createTempFile("SecureAcceptAllGetTest", ".truststore"); FileOutputStream keystoreFileOut = new FileOutputStream(keystore); try { keystoreFileOut.write(sampleKeystore1); } finally { keystoreFileOut.close(); } FileOutputStream truststoreFileOut = new FileOutputStream(truststore); try { truststoreFileOut.write(sampleTruststore1); } finally { truststoreFileOut.close(); } KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(keystore), PASSWORD.toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, PASSWORD.toCharArray()); KeyStore ts = KeyStore.getInstance("JKS"); ts.load(new FileInputStream(truststore), PASSWORD.toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ts); SSLContext sc = SSLContext.getInstance("TLS"); sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); HttpsServer secureServer = HttpsServer.create(inetSocketAddress, 0); secureServer.setHttpsConfigurator(new HttpsConfigurator(sc) { public void configure (HttpsParameters params) { SSLContext c = getSSLContext(); SSLParameters sslparams = c.getDefaultSSLParameters(); params.setSSLParameters(sslparams); } }); server = secureServer; } else { server = HttpServer.create(inetSocketAddress, 0); } server.setExecutor(service); for (Entry<String, HttpHandler> handler : handlers.entrySet()) { server.createContext(handler.getKey(), handler.getValue()); } server.start(); localHttpServerPort = server.getAddress().getPort(); System.out.println(description.getClassName() + " TestServer is started: " + getServerUrl()); } public void after(final Description description) { try{ server.stop(0); ((ExecutorService) server.getExecutor()).shutdownNow(); System.out.println(description.getClassName() + " TestServer is shutdown: " + getServerUrl()); } catch (Exception e) { e.printStackTrace(); } } /** * @return Get the root server URL */ public String getServerUrl() { if (hasSsl) { return "https://localhost:" + localHttpServerPort; } else { return "http://localhost:" + localHttpServerPort; } } /** * @return Get the root server URL * @throws URISyntaxException */ public URI getServerURI() throws URISyntaxException { return new URI(getServerUrl()); } /** * @param path * @return Get a path to this server */ public String getServerPath(String path) { return getServerUrl() + path; } /** * @param path * @return Get a path to this server */ public URI getServerPathURI(String path) throws URISyntaxException { return new URI(getServerUrl() + path); } /** * @return Return the ephemeral port used by this server */ public int getServerPort() { return localHttpServerPort; } public File getKeyStore() { return keystore; } public File getTrustStore() { return truststore; } }