diff --git a/CHANGES.md b/CHANGES.md index 4f4fa17..7a156ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Version 2.1 (unreleased) +Servlet API 6.0.0 is now the minimum servlet API officially supported. +Version 5 should on a "best effort" basis. + +\#259: The SameSite attribute in cookies is now preserved. + # Version 2.0 released on 2023-06-28 \#231: Added support of preserveCookiePath configuration parameter. It allows to keep cookie path unchanged in Set-Cookie server response header. diff --git a/README.md b/README.md index 51fa79e..a816d61 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ See [CHANGES.md](CHANGES.md) for a history of changes. Build & Installation ------------ +**Java version:** The minimum Java version is 11. + Simply build the jar using "mvn package" at the command line. The jar is built to "target/smiley-http-proxy-servlet-VERSION.jar". You don't have to build the jar if you aren't modifying the code, since released diff --git a/pom.xml b/pom.xml index 957fbe6..4037552 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ jakarta.servlet jakarta.servlet-api - 5.0.0 + 6.0.0 provided @@ -103,9 +103,9 @@ - org.eclipse.jetty - jetty-servlet - 11.0.15 + org.apache.tomcat.embed + tomcat-embed-core + 10.1.54 test diff --git a/src/main/java/org/mitre/dsmiley/httpproxy/ProxyServlet.java b/src/main/java/org/mitre/dsmiley/httpproxy/ProxyServlet.java index 41b2983..1eec923 100755 --- a/src/main/java/org/mitre/dsmiley/httpproxy/ProxyServlet.java +++ b/src/main/java/org/mitre/dsmiley/httpproxy/ProxyServlet.java @@ -582,10 +582,28 @@ protected void copyProxyCookie(HttpServletRequest servletRequest, HttpServletResponse servletResponse, String headerValue) { for (HttpCookie cookie : HttpCookie.parse(headerValue)) { Cookie servletCookie = createProxyCookie(servletRequest, cookie); + String sameSite = parseSameSite(headerValue); + if (sameSite != null) { + try { + servletCookie.setAttribute("SameSite", sameSite); // Servlet 6.0+ + } catch (NoSuchMethodError ignored) { + // SameSite not preserved on older versions (e.g. Servlet 5.0) + } + } servletResponse.addCookie(servletCookie); } } + private static String parseSameSite(String headerValue) { + for (String part : headerValue.split(";")) { + String trimmed = part.trim(); + if (trimmed.regionMatches(true, 0, "SameSite=", 0, 9)) { + return trimmed.substring(9).trim(); + } + } + return null; + } + /** * Creates a proxy cookie from the original cookie. * diff --git a/src/test/java/org/mitre/dsmiley/httpproxy/AcceptEncodingTest.java b/src/test/java/org/mitre/dsmiley/httpproxy/AcceptEncodingTest.java index 04820e5..acc07d7 100644 --- a/src/test/java/org/mitre/dsmiley/httpproxy/AcceptEncodingTest.java +++ b/src/test/java/org/mitre/dsmiley/httpproxy/AcceptEncodingTest.java @@ -26,39 +26,39 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.junit.After; import org.junit.Before; import org.junit.Test; public class AcceptEncodingTest { - private Server server; - private ServletHandler servletHandler; + private Tomcat tomcat; + private Context ctx; private int serverPort; @Before public void setUp() throws Exception { - server = new Server(0); - servletHandler = new ServletHandler(); - Handler serverHandler = servletHandler; - server.setHandler(serverHandler); - server.start(); - - serverPort = ((ServerConnector) server.getConnectors()[0]).getLocalPort(); + tomcat = new Tomcat(); + tomcat.setPort(0); + String tempDir = System.getProperty("java.io.tmpdir"); + tomcat.setBaseDir( tempDir ); + tomcat.getConnector(); + ctx = tomcat.addContext("", tempDir ); + tomcat.start(); + serverPort = tomcat.getConnector().getLocalPort(); } @After public void tearDown() throws Exception { - server.stop(); + tomcat.stop(); + tomcat.destroy(); serverPort = -1; } @@ -78,23 +78,25 @@ public void testHandlingAcceptEncodingHeader() throws Exception { of the client needs to be passed through as is. */ - ServletHolder servletHolder = servletHandler.addServletWithMapping(ProxyServlet.class, "/acceptEncodingProxyHandleCompression/*"); - servletHolder.setInitParameter(ProxyServlet.P_LOG, "true"); - servletHolder.setInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/acceptEncoding/", serverPort)); - servletHolder.setInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.TRUE.toString()); + Wrapper w1 = Tomcat.addServlet(ctx, "proxy1", ProxyServlet.class.getName()); + w1.addInitParameter(ProxyServlet.P_LOG, "true"); + w1.addInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/acceptEncoding/", serverPort)); + w1.addInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.TRUE.toString()); + ctx.addServletMappingDecoded("/acceptEncodingProxyHandleCompression/*", "proxy1"); - ServletHolder servletHolder2 = servletHandler.addServletWithMapping(ProxyServlet.class, "/acceptEncodingProxy/*"); - servletHolder2.setInitParameter(ProxyServlet.P_LOG, "true"); - servletHolder2.setInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/acceptEncoding/", serverPort)); - servletHolder2.setInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.FALSE.toString()); + Wrapper w2 = Tomcat.addServlet(ctx, "proxy2", ProxyServlet.class.getName()); + w2.addInitParameter(ProxyServlet.P_LOG, "true"); + w2.addInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/acceptEncoding/", serverPort)); + w2.addInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.FALSE.toString()); + ctx.addServletMappingDecoded("/acceptEncodingProxy/*", "proxy2"); - ServletHolder dummyBackend = new ServletHolder(new HttpServlet() { + Tomcat.addServlet(ctx, "backend", new HttpServlet() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getOutputStream().write(req.getHeader("Accept-Encoding").getBytes(StandardCharsets.UTF_8)); } }); - servletHandler.addServletWithMapping(dummyBackend, "/acceptEncoding/*"); + ctx.addServletMappingDecoded("/acceptEncoding/*", "backend"); HttpGet queryHandleCompression = new HttpGet(String.format("http://localhost:%d/acceptEncodingProxyHandleCompression/test", serverPort)); HttpGet query = new HttpGet(String.format("http://localhost:%d/acceptEncodingProxy/test", serverPort)); @@ -106,7 +108,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se try (CloseableHttpClient chc = HttpClientBuilder.create().disableContentCompression().build(); CloseableHttpResponse responseHandleCompression = chc.execute(queryHandleCompression); - CloseableHttpResponse response = chc.execute(query); + CloseableHttpResponse response = chc.execute(query) ) { try (InputStream is = response.getEntity().getContent()) { byte[] readData = readBlock(is); diff --git a/src/test/java/org/mitre/dsmiley/httpproxy/ChunkedTransferTest.java b/src/test/java/org/mitre/dsmiley/httpproxy/ChunkedTransferTest.java index 91b538b..83ce79a 100644 --- a/src/test/java/org/mitre/dsmiley/httpproxy/ChunkedTransferTest.java +++ b/src/test/java/org/mitre/dsmiley/httpproxy/ChunkedTransferTest.java @@ -16,6 +16,7 @@ package org.mitre.dsmiley.httpproxy; import java.io.IOException; +import java.util.zip.GZIPOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -23,37 +24,43 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; import org.apache.http.MalformedChunkCodingException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; -import org.eclipse.jetty.server.Handler; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; -import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class ChunkedTransferTest { @Parameters public static List data() { - return Arrays.asList(new Object[][] { + return Arrays.asList(new Object[][] { {false, false}, {false, true}, {true, false}, @@ -61,11 +68,11 @@ public static List data() { }); } - private Server server; - private ServletHandler servletHandler; + private Tomcat tomcat; + private Context ctx; private int serverPort; - private boolean supportBackendCompression; - private boolean handleCompressionApacheClient; + private final boolean supportBackendCompression; + private final boolean handleCompressionApacheClient; public ChunkedTransferTest(boolean supportBackendCompression, boolean handleCompressionApacheClient) { this.supportBackendCompression = supportBackendCompression; @@ -74,26 +81,32 @@ public ChunkedTransferTest(boolean supportBackendCompression, boolean handleComp @Before public void setUp() throws Exception { - server = new Server(0); - servletHandler = new ServletHandler(); - Handler serverHandler = servletHandler; - if(supportBackendCompression) { - GzipHandler gzipHandler = new GzipHandler(); - gzipHandler.setHandler(serverHandler); - gzipHandler.setSyncFlush(true); - serverHandler = gzipHandler; - } else { - serverHandler = servletHandler; + tomcat = new Tomcat(); + tomcat.setPort(0); + String tempDir = System.getProperty("java.io.tmpdir"); + tomcat.setBaseDir(tempDir); + tomcat.getConnector(); + ctx = tomcat.addContext("", tempDir); + + if (supportBackendCompression) { + FilterDef filterDef = new FilterDef(); + filterDef.setFilterName("gzip"); + filterDef.setFilter(new GzipFilter()); + ctx.addFilterDef(filterDef); + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName("gzip"); + filterMap.addURLPattern("/chat/*"); + ctx.addFilterMap(filterMap); } - server.setHandler(serverHandler); - server.start(); - serverPort = ((ServerConnector) server.getConnectors()[0]).getLocalPort(); + tomcat.start(); + serverPort = tomcat.getConnector().getLocalPort(); } @After public void tearDown() throws Exception { - server.stop(); + tomcat.stop(); + tomcat.destroy(); serverPort = -1; } @@ -109,7 +122,7 @@ public void testChunkedTransfer() throws Exception { received and further data must not be present. After the first message is consumed, the CountDownLatch is released and - the second messsage is expected. This in turn must be completely be read. + the second message is expected. This in turn must be completely be read. If the CountDownLatch is not released, it will timeout and the second message will not be send. @@ -119,12 +132,13 @@ public void testChunkedTransfer() throws Exception { final byte[] data1 = "event: message\ndata: Dummy Data1\n\n".getBytes(StandardCharsets.UTF_8); final byte[] data2 = "event: message\ndata: Dummy Data2\n\n".getBytes(StandardCharsets.UTF_8); - ServletHolder servletHolder = servletHandler.addServletWithMapping(ProxyServlet.class, "/chatProxied/*"); - servletHolder.setInitParameter(ProxyServlet.P_LOG, "true"); - servletHolder.setInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/chat/", serverPort)); - servletHolder.setInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.toString(handleCompressionApacheClient)); + Wrapper proxyWrapper = Tomcat.addServlet(ctx, "proxy", ProxyServlet.class.getName()); + proxyWrapper.addInitParameter(ProxyServlet.P_LOG, "true"); + proxyWrapper.addInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/chat/", serverPort)); + proxyWrapper.addInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.toString(handleCompressionApacheClient)); + ctx.addServletMappingDecoded("/chatProxied/*", "proxy"); - ServletHolder dummyBackend = new ServletHolder(new HttpServlet() { + Tomcat.addServlet(ctx, "backend", new HttpServlet() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/event-stream"); @@ -145,7 +159,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } } }); - servletHandler.addServletWithMapping(dummyBackend, "/chat/*"); + ctx.addServletMappingDecoded("/chat/*", "backend"); HttpGet url = new HttpGet(String.format("http://localhost:%d/chatProxied/test", serverPort)); @@ -179,12 +193,13 @@ when the closing of the proxy (frontend) connection is detected. final byte[] data1 = "event: message\ndata: Dummy Data1\n\n".getBytes(StandardCharsets.UTF_8); final byte[] data2 = "event: message\ndata: Dummy Data2\n\n".getBytes(StandardCharsets.UTF_8); - ServletHolder servletHolder = servletHandler.addServletWithMapping(ProxyServlet.class, "/chatProxied/*"); - servletHolder.setInitParameter(ProxyServlet.P_LOG, "true"); - servletHolder.setInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/chat/", serverPort)); - servletHolder.setInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.toString(handleCompressionApacheClient)); + Wrapper proxyWrapper = Tomcat.addServlet(ctx, "proxy", ProxyServlet.class.getName()); + proxyWrapper.addInitParameter(ProxyServlet.P_LOG, "true"); + proxyWrapper.addInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/chat/", serverPort)); + proxyWrapper.addInitParameter(ProxyServlet.P_HANDLECOMPRESSION, Boolean.toString(handleCompressionApacheClient)); + ctx.addServletMappingDecoded("/chatProxied/*", "proxy"); - ServletHolder dummyBackend = new ServletHolder(new HttpServlet() { + Tomcat.addServlet(ctx, "backend", new HttpServlet() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/event-stream"); @@ -196,11 +211,11 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se // Wait for client to request the second message by counting down the // latch - if the latch times out, the second message will not be // send and the corresponding assert will fail - if (! guardForSecondRead.await(10, TimeUnit.SECONDS)) { + if (!guardForSecondRead.await(10, TimeUnit.SECONDS)) { throw new IOException("Wait timed out"); } try { - for(int i = 0; i < 100; i++) { + for (int i = 0; i < 100; i++) { os.write(data2); os.flush(); Thread.sleep(100); @@ -215,7 +230,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } } }); - servletHandler.addServletWithMapping(dummyBackend, "/chat/*"); + ctx.addServletMappingDecoded("/chat/*", "backend"); HttpGet url = new HttpGet(String.format("http://localhost:%d/chatProxied/test", serverPort)); @@ -247,4 +262,55 @@ private static byte[] readBlock(InputStream is) throws IOException { int read = is.read(buffer); return Arrays.copyOfRange(buffer, 0, read); } + + // Wraps the backend response in GZIP encoding with sync-flush so chunked + // data is flushed to the client incrementally rather than buffered. + private static class GzipFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setHeader("Content-Encoding", "gzip"); + GzipResponseWrapper wrapper = new GzipResponseWrapper(httpResponse); + try { + chain.doFilter(request, wrapper); + } finally { + try { wrapper.finish(); } catch (IOException ignored) {} + } + } + + @Override + public void destroy() {} + } + + private static class GzipResponseWrapper extends HttpServletResponseWrapper { + private final GZIPOutputStream gzipOut; + private final ServletOutputStream servletOut; + + GzipResponseWrapper(HttpServletResponse response) throws IOException { + super(response); + // true = syncFlush: each flush() also flushes the underlying gzip stream + gzipOut = new GZIPOutputStream(response.getOutputStream(), true); + servletOut = new ServletOutputStream() { + @Override + public void write(int b) throws IOException { gzipOut.write(b); } + @Override + public void write(byte[] b, int off, int len) throws IOException { gzipOut.write(b, off, len); } + @Override + public void flush() throws IOException { gzipOut.flush(); } + @Override + public boolean isReady() { return true; } + @Override + public void setWriteListener(WriteListener writeListener) {} + }; + } + + @Override + public ServletOutputStream getOutputStream() { return servletOut; } + + void finish() throws IOException { gzipOut.finish(); } + } } diff --git a/src/test/java/org/mitre/dsmiley/httpproxy/CookieSameSiteTest.java b/src/test/java/org/mitre/dsmiley/httpproxy/CookieSameSiteTest.java new file mode 100644 index 0000000..5ab0282 --- /dev/null +++ b/src/test/java/org/mitre/dsmiley/httpproxy/CookieSameSiteTest.java @@ -0,0 +1,86 @@ +/* + * 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 org.mitre.dsmiley.httpproxy; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.http.Header; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CookieSameSiteTest { + + private Tomcat tomcat; + private Context ctx; + private int serverPort; + + @Before + public void setUp() throws Exception { + tomcat = new Tomcat(); + tomcat.setPort(0); + String tempDir = System.getProperty("java.io.tmpdir"); + tomcat.setBaseDir(tempDir); + tomcat.getConnector(); + ctx = tomcat.addContext("", tempDir); + tomcat.start(); + serverPort = tomcat.getConnector().getLocalPort(); + } + + @After + public void tearDown() throws Exception { + tomcat.stop(); + tomcat.destroy(); + serverPort = -1; + } + + @Test + public void testSameSiteAttributeIsPreserved() throws Exception { + Tomcat.addServlet(ctx, "backend", new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.addHeader("Set-Cookie", "JSESSIONID=1234; Path=/backend; SameSite=Strict"); + } + }); + ctx.addServletMappingDecoded("/backend/*", "backend"); + + Wrapper proxyWrapper = Tomcat.addServlet(ctx, "proxy", ProxyServlet.class.getName()); + proxyWrapper.addInitParameter(ProxyServlet.P_TARGET_URI, + String.format("http://localhost:%d/backend/", serverPort)); + ctx.addServletMappingDecoded("/proxy/*", "proxy"); + + HttpGet request = new HttpGet(String.format("http://localhost:%d/proxy/test", serverPort)); + try (CloseableHttpClient client = HttpClientBuilder.create().disableRedirectHandling().build(); + CloseableHttpResponse response = client.execute(request)) { + Header setCookieHeader = response.getFirstHeader("Set-Cookie"); + assertNotNull("Set-Cookie header must be present", setCookieHeader); + assertTrue("SameSite attribute must be preserved when proxying cookies", + setCookieHeader.getValue().contains("SameSite=Strict")); + } + } +} diff --git a/src/test/java/org/mitre/dsmiley/httpproxy/ParallelConnectionsTest.java b/src/test/java/org/mitre/dsmiley/httpproxy/ParallelConnectionsTest.java index 967b93b..c86703f 100644 --- a/src/test/java/org/mitre/dsmiley/httpproxy/ParallelConnectionsTest.java +++ b/src/test/java/org/mitre/dsmiley/httpproxy/ParallelConnectionsTest.java @@ -15,7 +15,6 @@ */ package org.mitre.dsmiley.httpproxy; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -35,42 +34,45 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; import org.junit.After; + import static org.junit.Assert.assertEquals; + import org.junit.Before; import org.junit.Test; public class ParallelConnectionsTest { - private Server server; - private ServletHandler servletHandler; + private Tomcat tomcat; + private Context ctx; private int serverPort; @Before public void setUp() throws Exception { - server = new Server(0); - servletHandler = new ServletHandler(); - server.setHandler(servletHandler); - server.start(); - - serverPort = ((ServerConnector) server.getConnectors()[0]).getLocalPort(); + tomcat = new Tomcat(); + tomcat.setPort(0); + String tempDir = System.getProperty("java.io.tmpdir"); + tomcat.setBaseDir(tempDir); + tomcat.getConnector(); + ctx = tomcat.addContext("", tempDir); + tomcat.start(); + serverPort = tomcat.getConnector().getLocalPort(); } @After public void tearDown() throws Exception { - server.stop(); + tomcat.stop(); + tomcat.destroy(); serverPort = -1; } @Test public void testHandlingMultipleConnectionsSameRoute() throws Exception { /* - This test ensures, that a minimum nunmber of parallel connections can be + This test ensures, that a minimum number of parallel connections can be established. The test works by opening "parallelConnectionsToTest" connections, this is enforced by a countdownlatch, that ensures, that all connections to the backend are established before they are served by the @@ -79,34 +81,41 @@ public void testHandlingMultipleConnectionsSameRoute() throws Exception { int parallelConnectionsToTest = 10; - ServletHolder servletHolder = servletHandler.addServletWithMapping(ProxyServlet.class, "/sampleBackendProxied/*"); - servletHolder.setInitParameter(ProxyServlet.P_LOG, "true"); - servletHolder.setInitParameter(ProxyServlet.P_MAXCONNECTIONS, Integer.toString(parallelConnectionsToTest)); - servletHolder.setInitParameter(ProxyServlet.P_TARGET_URI, String.format("http://localhost:%d/sampleBackend/", serverPort)); + Wrapper proxyWrapper = Tomcat.addServlet(ctx, "proxy", ProxyServlet.class.getName()); + proxyWrapper.addInitParameter(ProxyServlet.P_LOG, "true"); + proxyWrapper.addInitParameter( + ProxyServlet.P_MAXCONNECTIONS, + Integer.toString(parallelConnectionsToTest)); + proxyWrapper.addInitParameter( + ProxyServlet.P_TARGET_URI, + String.format("http://localhost:%d/sampleBackend/", serverPort)); + ctx.addServletMappingDecoded("/sampleBackendProxied/*", "proxy"); CountDownLatch requestsReceived = new CountDownLatch(parallelConnectionsToTest); - ServletHolder dummyBackend = new ServletHolder(new HttpServlet() { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - try { - // The latch ensures, that all servlets wait until all expected - // connections are made. Only after all clients have connected, the - // request is fullfilled. - requestsReceived.countDown(); - if (requestsReceived.await(10, TimeUnit.SECONDS)) { - resp.setHeader("Content-Type", "text/plain; charset=utf-8"); - OutputStream os = resp.getOutputStream(); - OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8); - osw.write("Works"); - osw.flush(); + Tomcat.addServlet( + ctx, "backend", new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + try { + // The latch ensures, that all servlets wait until all expected + // connections are made. Only after all clients have connected, the + // request is fulfilled. + requestsReceived.countDown(); + if (requestsReceived.await(10, TimeUnit.SECONDS)) { + resp.setHeader("Content-Type", "text/plain; charset=utf-8"); + OutputStream os = resp.getOutputStream(); + OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8); + osw.write("Works"); + osw.flush(); + } + } catch (InterruptedException ex) { + throw new IOException(ex); + } } - } catch (InterruptedException ex) { - throw new IOException(ex); - } - } - }); - servletHandler.addServletWithMapping(dummyBackend, "/sampleBackend/*"); + }); + ctx.addServletMappingDecoded("/sampleBackend/*", "backend"); URL url = new URL(String.format("http://localhost:%d/sampleBackendProxied/test", serverPort)); @@ -119,7 +128,7 @@ public String call() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[10 * 1024]; int read; - while((read = is.read(buffer)) > 0) { + while ((read = is.read(buffer)) > 0) { baos.write(buffer, 0, read); } return baos.toString("UTF-8"); @@ -129,11 +138,11 @@ public String call() throws Exception { List> result = new ArrayList<>(parallelConnectionsToTest); - for(int i = 0; i < parallelConnectionsToTest; i++) { + for (int i = 0; i < parallelConnectionsToTest; i++) { result.add(es.submit(new Client())); } - for(Future f: result) { + for (Future f : result) { assertEquals("Works", f.get()); } }