| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.proxy; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InterruptedIOException; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.net.ConnectException; |
| import java.net.HttpCookie; |
| import java.nio.ByteBuffer; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.StandardOpenOption; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.zip.GZIPOutputStream; |
| |
| import javax.servlet.AsyncContext; |
| import javax.servlet.AsyncEvent; |
| import javax.servlet.AsyncListener; |
| import javax.servlet.DispatcherType; |
| import javax.servlet.Filter; |
| import javax.servlet.FilterChain; |
| import javax.servlet.FilterConfig; |
| import javax.servlet.ServletException; |
| import javax.servlet.ServletOutputStream; |
| import javax.servlet.ServletRequest; |
| import javax.servlet.ServletResponse; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.eclipse.jetty.client.HttpClient; |
| import org.eclipse.jetty.client.HttpContentResponse; |
| import org.eclipse.jetty.client.HttpProxy; |
| import org.eclipse.jetty.client.api.ContentResponse; |
| import org.eclipse.jetty.client.api.Request; |
| import org.eclipse.jetty.client.api.Response; |
| import org.eclipse.jetty.client.api.Result; |
| import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; |
| import org.eclipse.jetty.client.util.BufferingResponseListener; |
| import org.eclipse.jetty.client.util.BytesContentProvider; |
| import org.eclipse.jetty.client.util.InputStreamResponseListener; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.http.HttpMethod; |
| import org.eclipse.jetty.server.HttpConfiguration; |
| import org.eclipse.jetty.server.HttpConnectionFactory; |
| import org.eclipse.jetty.server.Server; |
| import org.eclipse.jetty.server.ServerConnector; |
| import org.eclipse.jetty.servlet.FilterHolder; |
| import org.eclipse.jetty.servlet.ServletContextHandler; |
| import org.eclipse.jetty.servlet.ServletHolder; |
| import org.eclipse.jetty.toolchain.test.MavenTestingUtils; |
| import org.eclipse.jetty.toolchain.test.TestTracker; |
| import org.eclipse.jetty.util.Callback; |
| import org.eclipse.jetty.util.IO; |
| import org.eclipse.jetty.util.thread.QueuedThreadPool; |
| import org.hamcrest.Matchers; |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class ProxyServletTest |
| { |
| private static final String PROXIED_HEADER = "X-Proxied"; |
| |
| @Parameterized.Parameters(name = "{0}") |
| public static Iterable<Object[]> data() |
| { |
| return Arrays.asList(new Object[][]{ |
| {ProxyServlet.class}, |
| {AsyncProxyServlet.class}, |
| {AsyncMiddleManServlet.class} |
| }); |
| } |
| |
| @Rule |
| public final TestTracker tracker = new TestTracker(); |
| private HttpClient client; |
| private Server proxy; |
| private ServerConnector proxyConnector; |
| private ServletContextHandler proxyContext; |
| private AbstractProxyServlet proxyServlet; |
| private Server server; |
| private ServerConnector serverConnector; |
| |
| public ProxyServletTest(Class<?> proxyServletClass) throws Exception |
| { |
| this.proxyServlet = (AbstractProxyServlet)proxyServletClass.newInstance(); |
| } |
| |
| private void startServer(HttpServlet servlet) throws Exception |
| { |
| QueuedThreadPool serverPool = new QueuedThreadPool(); |
| serverPool.setName("server"); |
| server = new Server(serverPool); |
| serverConnector = new ServerConnector(server); |
| server.addConnector(serverConnector); |
| |
| ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); |
| ServletHolder appServletHolder = new ServletHolder(servlet); |
| appCtx.addServlet(appServletHolder, "/*"); |
| |
| server.start(); |
| } |
| |
| private void startProxy() throws Exception |
| { |
| startProxy(new HashMap<>()); |
| } |
| |
| private void startProxy(Map<String, String> initParams) throws Exception |
| { |
| QueuedThreadPool proxyPool = new QueuedThreadPool(); |
| proxyPool.setName("proxy"); |
| proxy = new Server(proxyPool); |
| |
| HttpConfiguration configuration = new HttpConfiguration(); |
| configuration.setSendDateHeader(false); |
| configuration.setSendServerVersion(false); |
| String value = initParams.get("outputBufferSize"); |
| if (value != null) |
| configuration.setOutputBufferSize(Integer.valueOf(value)); |
| proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration)); |
| proxy.addConnector(proxyConnector); |
| |
| proxyContext = new ServletContextHandler(proxy, "/", true, false); |
| ServletHolder proxyServletHolder = new ServletHolder(proxyServlet); |
| proxyServletHolder.setInitParameters(initParams); |
| proxyContext.addServlet(proxyServletHolder, "/*"); |
| |
| proxy.start(); |
| } |
| |
| private void startClient() throws Exception |
| { |
| client = prepareClient(); |
| } |
| |
| private HttpClient prepareClient() throws Exception |
| { |
| QueuedThreadPool clientPool = new QueuedThreadPool(); |
| clientPool.setName("client"); |
| HttpClient result = new HttpClient(); |
| result.setExecutor(clientPool); |
| result.getProxyConfiguration().getProxies().add(new HttpProxy("localhost", proxyConnector.getLocalPort())); |
| result.start(); |
| return result; |
| } |
| |
| @After |
| public void dispose() throws Exception |
| { |
| client.stop(); |
| proxy.stop(); |
| server.stop(); |
| } |
| |
| @Test |
| public void testProxyDown() throws Exception |
| { |
| startServer(new EmptyHttpServlet()); |
| startProxy(); |
| startClient(); |
| // Shutdown the proxy |
| proxy.stop(); |
| |
| try |
| { |
| client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.fail(); |
| } |
| catch (ExecutionException x) |
| { |
| Assert.assertThat(x.getCause(), Matchers.instanceOf(ConnectException.class)); |
| } |
| } |
| |
| @Test |
| public void testProxyWithoutContent() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.assertEquals("OK", response.getReason()); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testProxyWithResponseContent() throws Exception |
| { |
| final byte[] content = new byte[1024]; |
| new Random().nextBytes(content); |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| resp.getOutputStream().write(content); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| ContentResponse[] responses = new ContentResponse[10]; |
| for (int i = 0; i < 10; ++i) |
| { |
| // Request is for the target server |
| responses[i] = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| } |
| |
| for (int i = 0; i < 10; ++i) |
| { |
| Assert.assertEquals(200, responses[i].getStatus()); |
| Assert.assertTrue(responses[i].getHeaders().containsKey(PROXIED_HEADER)); |
| Assert.assertArrayEquals(content, responses[i].getContent()); |
| } |
| } |
| |
| @Test |
| public void testProxyWithRequestContentAndResponseContent() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| IO.copy(req.getInputStream(), resp.getOutputStream()); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| byte[] content = new byte[1024]; |
| new Random().nextBytes(content); |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .method(HttpMethod.POST) |
| .content(new BytesContentProvider(content)) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| Assert.assertArrayEquals(content, response.getContent()); |
| } |
| |
| @Test |
| public void testProxyWithBigRequestContentIgnored() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| try |
| { |
| // Give some time to the proxy to |
| // upload the content to the server. |
| Thread.sleep(1000); |
| |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| } |
| catch (InterruptedException x) |
| { |
| throw new InterruptedIOException(); |
| } |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| byte[] content = new byte[128 * 1024]; |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .method(HttpMethod.POST) |
| .content(new BytesContentProvider(content)) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testProxyWithBigRequestContentConsumed() throws Exception |
| { |
| final byte[] content = new byte[128 * 1024]; |
| new Random().nextBytes(content); |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| InputStream input = req.getInputStream(); |
| int index = 0; |
| while (true) |
| { |
| int value = input.read(); |
| if (value < 0) |
| break; |
| Assert.assertEquals("Content mismatch at index=" + index, content[index] & 0xFF, value); |
| ++index; |
| } |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .method(HttpMethod.POST) |
| .content(new BytesContentProvider(content)) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testProxyWithBigResponseContentWithSlowReader() throws Exception |
| { |
| // Create a 6 MiB file |
| final int length = 6 * 1024; |
| Path targetTestsDir = MavenTestingUtils.getTargetTestingDir().toPath(); |
| Files.createDirectories(targetTestsDir); |
| final Path temp = Files.createTempFile(targetTestsDir, "test_", null); |
| byte[] kb = new byte[1024]; |
| new Random().nextBytes(kb); |
| try (OutputStream output = Files.newOutputStream(temp, StandardOpenOption.CREATE)) |
| { |
| for (int i = 0; i < length; ++i) |
| output.write(kb); |
| } |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException |
| { |
| try (InputStream input = Files.newInputStream(temp)) |
| { |
| IO.copy(input, response.getOutputStream()); |
| } |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| Request request = client.newRequest("localhost", serverConnector.getLocalPort()).path("/proxy/test"); |
| final CountDownLatch latch = new CountDownLatch(1); |
| request.send(new BufferingResponseListener(2 * length * 1024) |
| { |
| @Override |
| public void onContent(Response response, ByteBuffer content) |
| { |
| try |
| { |
| // Slow down the reader |
| TimeUnit.MILLISECONDS.sleep(5); |
| super.onContent(response, content); |
| } |
| catch (InterruptedException x) |
| { |
| response.abort(x); |
| } |
| } |
| |
| @Override |
| public void onComplete(Result result) |
| { |
| Assert.assertFalse(result.isFailed()); |
| Assert.assertEquals(200, result.getResponse().getStatus()); |
| Assert.assertEquals(length * 1024, getContent().length); |
| latch.countDown(); |
| } |
| }); |
| Assert.assertTrue(latch.await(30, TimeUnit.SECONDS)); |
| } |
| |
| @Test |
| public void testProxyWithQueryString() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| resp.getOutputStream().print(req.getQueryString()); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| String query = "a=1&b=%E2%82%AC"; |
| ContentResponse response = client.newRequest("http://localhost:" + serverConnector.getLocalPort() + "/?" + query) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertEquals(query, response.getContentAsString()); |
| } |
| |
| @Test |
| public void testProxyLongPoll() throws Exception |
| { |
| final long timeout = 1000; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException |
| { |
| if (!request.isAsyncStarted()) |
| { |
| final AsyncContext asyncContext = request.startAsync(); |
| asyncContext.setTimeout(timeout); |
| asyncContext.addListener(new AsyncListener() |
| { |
| @Override |
| public void onComplete(AsyncEvent event) throws IOException |
| { |
| } |
| |
| @Override |
| public void onTimeout(AsyncEvent event) throws IOException |
| { |
| if (request.getHeader("Via") != null) |
| response.addHeader(PROXIED_HEADER, "true"); |
| asyncContext.complete(); |
| } |
| |
| @Override |
| public void onError(AsyncEvent event) throws IOException |
| { |
| } |
| |
| @Override |
| public void onStartAsync(AsyncEvent event) throws IOException |
| { |
| } |
| }); |
| } |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| Response response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(2 * timeout, TimeUnit.MILLISECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testProxyXForwardedHostHeaderIsPresent() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| PrintWriter writer = resp.getWriter(); |
| writer.write(req.getHeader("X-Forwarded-Host")); |
| writer.flush(); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort()); |
| Assert.assertThat("Response expected to contain content of X-Forwarded-Host Header from the request", |
| response.getContentAsString(), |
| Matchers.equalTo("localhost:" + serverConnector.getLocalPort())); |
| } |
| |
| @Test |
| public void testProxyWhiteList() throws Exception |
| { |
| startServer(new EmptyHttpServlet()); |
| startProxy(); |
| startClient(); |
| int port = serverConnector.getLocalPort(); |
| proxyServlet.getWhiteListHosts().add("127.0.0.1:" + port); |
| |
| // Try with the wrong host |
| ContentResponse response = client.newRequest("localhost", port) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(403, response.getStatus()); |
| |
| // Try again with the right host |
| response = client.newRequest("127.0.0.1", port) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| } |
| |
| @Test |
| public void testProxyBlackList() throws Exception |
| { |
| startServer(new EmptyHttpServlet()); |
| startProxy(); |
| startClient(); |
| int port = serverConnector.getLocalPort(); |
| proxyServlet.getBlackListHosts().add("localhost:" + port); |
| |
| // Try with the wrong host |
| ContentResponse response = client.newRequest("localhost", port) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(403, response.getStatus()); |
| |
| // Try again with the right host |
| response = client.newRequest("127.0.0.1", port) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| } |
| |
| @Test |
| public void testClientExcludedHosts() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| int port = serverConnector.getLocalPort(); |
| client.getProxyConfiguration().getProxies().get(0).getExcludedAddresses().add("127.0.0.1:" + port); |
| |
| // Try with a proxied host |
| ContentResponse response = client.newRequest("localhost", port) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| |
| // Try again with an excluded host |
| response = client.newRequest("127.0.0.1", port) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertFalse(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testTransparentProxy() throws Exception |
| { |
| testTransparentProxyWithPrefix("/proxy"); |
| } |
| |
| @Test |
| public void testTransparentProxyWithRootContext() throws Exception |
| { |
| testTransparentProxyWithPrefix("/"); |
| } |
| |
| private void testTransparentProxyWithPrefix(String prefix) throws Exception |
| { |
| final String target = "/test"; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| resp.setStatus(target.equals(req.getRequestURI()) ? 200 : 404); |
| } |
| }); |
| String proxyTo = "http://localhost:" + serverConnector.getLocalPort(); |
| proxyServlet = new ProxyServlet.Transparent(); |
| Map<String, String> params = new HashMap<>(); |
| params.put("proxyTo", proxyTo); |
| params.put("prefix", prefix); |
| startProxy(params); |
| startClient(); |
| |
| // Make the request to the proxy, it should transparently forward to the server |
| ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()) |
| .path((prefix + target).replaceAll("//", "/")) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testTransparentProxyWithQuery() throws Exception |
| { |
| testTransparentProxyWithQuery("/foo", "/proxy", "/test"); |
| } |
| |
| @Test |
| public void testTransparentProxyEmptyContextWithQuery() throws Exception |
| { |
| testTransparentProxyWithQuery("", "/proxy", "/test"); |
| } |
| |
| @Test |
| public void testTransparentProxyEmptyTargetWithQuery() throws Exception |
| { |
| testTransparentProxyWithQuery("/bar", "/proxy", ""); |
| } |
| |
| @Test |
| public void testTransparentProxyEmptyContextEmptyTargetWithQuery() throws Exception |
| { |
| testTransparentProxyWithQuery("", "/proxy", ""); |
| } |
| |
| private void testTransparentProxyWithQuery(String proxyToContext, String prefix, String target) throws Exception |
| { |
| final String query = "a=1&b=2"; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| |
| String expectedURI = proxyToContext + target; |
| if (expectedURI.isEmpty()) |
| expectedURI = "/"; |
| if (expectedURI.equals(req.getRequestURI())) |
| { |
| if (query.equals(req.getQueryString())) |
| { |
| resp.setStatus(200); |
| return; |
| } |
| } |
| resp.setStatus(404); |
| } |
| }); |
| String proxyTo = "http://localhost:" + serverConnector.getLocalPort() + proxyToContext; |
| proxyServlet = new ProxyServlet.Transparent(); |
| Map<String, String> params = new HashMap<>(); |
| params.put("proxyTo", proxyTo); |
| params.put("prefix", prefix); |
| startProxy(params); |
| startClient(); |
| |
| // Make the request to the proxy, it should transparently forward to the server |
| ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()) |
| .path(prefix + target + "?" + query) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testTransparentProxyWithQueryWithSpaces() throws Exception |
| { |
| final String target = "/test"; |
| final String query = "a=1&b=2&c=1234%205678&d=hello+world"; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| |
| if (target.equals(req.getRequestURI())) |
| { |
| if (query.equals(req.getQueryString())) |
| { |
| resp.setStatus(200); |
| return; |
| } |
| } |
| resp.setStatus(404); |
| } |
| }); |
| String proxyTo = "http://localhost:" + serverConnector.getLocalPort(); |
| String prefix = "/proxy"; |
| proxyServlet = new ProxyServlet.Transparent(); |
| Map<String, String> params = new HashMap<>(); |
| params.put("proxyTo", proxyTo); |
| params.put("prefix", prefix); |
| startProxy(params); |
| startClient(); |
| |
| // Make the request to the proxy, it should transparently forward to the server |
| ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()) |
| .path(prefix + target + "?" + query) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testTransparentProxyWithoutPrefix() throws Exception |
| { |
| final String target = "/test"; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| resp.setStatus(target.equals(req.getRequestURI()) ? 200 : 404); |
| } |
| }); |
| final String proxyTo = "http://localhost:" + serverConnector.getLocalPort(); |
| proxyServlet = new ProxyServlet.Transparent(); |
| Map<String, String> initParams = new HashMap<>(); |
| initParams.put("proxyTo", proxyTo); |
| startProxy(initParams); |
| startClient(); |
| |
| // Make the request to the proxy, it should transparently forward to the server |
| ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()) |
| .path(target) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testCachingProxy() throws Exception |
| { |
| final byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| resp.getOutputStream().write(content); |
| } |
| }); |
| // Don't do this at home: this example is not concurrent, not complete, |
| // it is only used for this test and to verify that ProxyServlet can be |
| // subclassed enough to write your own caching servlet |
| final String cacheHeader = "X-Cached"; |
| proxyServlet = new ProxyServlet() |
| { |
| private Map<String, ContentResponse> cache = new HashMap<>(); |
| private Map<String, ByteArrayOutputStream> temp = new HashMap<>(); |
| |
| @Override |
| protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException |
| { |
| ContentResponse cachedResponse = cache.get(request.getRequestURI()); |
| if (cachedResponse != null) |
| { |
| response.setStatus(cachedResponse.getStatus()); |
| // Should copy headers too, but keep it simple |
| response.addHeader(cacheHeader, "true"); |
| response.getOutputStream().write(cachedResponse.getContent()); |
| } |
| else |
| { |
| super.service(request, response); |
| } |
| } |
| |
| @Override |
| protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length, Callback callback) |
| { |
| // Accumulate the response content |
| ByteArrayOutputStream baos = temp.get(request.getRequestURI()); |
| if (baos == null) |
| { |
| baos = new ByteArrayOutputStream(); |
| temp.put(request.getRequestURI(), baos); |
| } |
| baos.write(buffer, offset, length); |
| super.onResponseContent(request, response, proxyResponse, buffer, offset, length, callback); |
| } |
| |
| @Override |
| protected void onProxyResponseSuccess(HttpServletRequest request, HttpServletResponse response, Response proxyResponse) |
| { |
| byte[] content = temp.remove(request.getRequestURI()).toByteArray(); |
| ContentResponse cached = new HttpContentResponse(proxyResponse, content, null, null); |
| cache.put(request.getRequestURI(), cached); |
| super.onProxyResponseSuccess(request, response, proxyResponse); |
| } |
| }; |
| startProxy(); |
| startClient(); |
| |
| // First request |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| Assert.assertArrayEquals(content, response.getContent()); |
| |
| // Second request should be cached |
| response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(cacheHeader)); |
| Assert.assertArrayEquals(content, response.getContent()); |
| } |
| |
| @Test |
| public void testRedirectsAreProxied() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| resp.sendRedirect("/"); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| client.setFollowRedirects(false); |
| |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(302, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| |
| @Test |
| public void testGZIPContentIsProxied() throws Exception |
| { |
| final byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| |
| resp.addHeader("Content-Encoding", "gzip"); |
| GZIPOutputStream gzipOutputStream = new GZIPOutputStream(resp.getOutputStream()); |
| gzipOutputStream.write(content); |
| gzipOutputStream.close(); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); |
| Assert.assertArrayEquals(content, response.getContent()); |
| } |
| |
| @Test(expected = TimeoutException.class) |
| public void testWrongContentLength() throws Exception |
| { |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| byte[] message = "tooshort".getBytes("ascii"); |
| resp.setContentType("text/plain;charset=ascii"); |
| resp.setHeader("Content-Length", Long.toString(message.length + 1)); |
| resp.getOutputStream().write(message); |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(1, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.fail(); |
| } |
| |
| @Test |
| public void testCookiesFromDifferentClientsAreNotMixed() throws Exception |
| { |
| final String name = "biscuit"; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| if (req.getHeader("Via") != null) |
| resp.addHeader(PROXIED_HEADER, "true"); |
| |
| String value = req.getHeader(name); |
| if (value != null) |
| { |
| Cookie cookie = new Cookie(name, value); |
| cookie.setMaxAge(3600); |
| resp.addCookie(cookie); |
| } |
| else |
| { |
| Cookie[] cookies = req.getCookies(); |
| Assert.assertEquals(1, cookies.length); |
| } |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| String value1 = "1"; |
| ContentResponse response1 = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .header(name, value1) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response1.getStatus()); |
| Assert.assertTrue(response1.getHeaders().containsKey(PROXIED_HEADER)); |
| List<HttpCookie> cookies = client.getCookieStore().getCookies(); |
| Assert.assertEquals(1, cookies.size()); |
| Assert.assertEquals(name, cookies.get(0).getName()); |
| Assert.assertEquals(value1, cookies.get(0).getValue()); |
| |
| HttpClient client2 = prepareClient(); |
| try |
| { |
| String value2 = "2"; |
| ContentResponse response2 = client2.newRequest("localhost", serverConnector.getLocalPort()) |
| .header(name, value2) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response2.getStatus()); |
| Assert.assertTrue(response2.getHeaders().containsKey(PROXIED_HEADER)); |
| cookies = client2.getCookieStore().getCookies(); |
| Assert.assertEquals(1, cookies.size()); |
| Assert.assertEquals(name, cookies.get(0).getName()); |
| Assert.assertEquals(value2, cookies.get(0).getValue()); |
| |
| // Make a third request to be sure the proxy does not mix cookies |
| ContentResponse response3 = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response3.getStatus()); |
| Assert.assertTrue(response3.getHeaders().containsKey(PROXIED_HEADER)); |
| } |
| finally |
| { |
| client2.stop(); |
| } |
| } |
| |
| @Test |
| public void testProxyRequestFailureInTheMiddleOfProxyingSmallContent() throws Exception |
| { |
| final CountDownLatch chunk1Latch = new CountDownLatch(1); |
| final int chunk1 = 'q'; |
| final int chunk2 = 'w'; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException |
| { |
| ServletOutputStream output = response.getOutputStream(); |
| output.write(chunk1); |
| response.flushBuffer(); |
| |
| // Wait for the client to receive this chunk. |
| await(chunk1Latch, 5000); |
| |
| // Send second chunk, must not be received by proxy. |
| output.write(chunk2); |
| } |
| |
| private boolean await(CountDownLatch latch, long ms) throws IOException |
| { |
| try |
| { |
| return latch.await(ms, TimeUnit.MILLISECONDS); |
| } |
| catch (InterruptedException x) |
| { |
| throw new InterruptedIOException(); |
| } |
| } |
| }); |
| final long proxyTimeout = 1000; |
| Map<String, String> proxyParams = new HashMap<>(); |
| proxyParams.put("timeout", String.valueOf(proxyTimeout)); |
| startProxy(proxyParams); |
| startClient(); |
| |
| InputStreamResponseListener listener = new InputStreamResponseListener(); |
| int port = serverConnector.getLocalPort(); |
| client.newRequest("localhost", port).send(listener); |
| |
| // Make the proxy request fail; given the small content, the |
| // proxy-to-client response is not committed yet so it will be reset. |
| TimeUnit.MILLISECONDS.sleep(2 * proxyTimeout); |
| |
| Response response = listener.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(504, response.getStatus()); |
| |
| // Make sure there is no content, as the proxy-to-client response has been reset. |
| InputStream input = listener.getInputStream(); |
| Assert.assertEquals(-1, input.read()); |
| |
| chunk1Latch.countDown(); |
| |
| // Result succeeds because a 504 is a valid HTTP response. |
| Result result = listener.await(5, TimeUnit.SECONDS); |
| Assert.assertTrue(result.isSucceeded()); |
| |
| // Make sure the proxy does not receive chunk2. |
| Assert.assertEquals(-1, input.read()); |
| |
| HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); |
| Assert.assertEquals(0, destination.getConnectionPool().getIdleConnections().size()); |
| } |
| |
| @Test |
| public void testProxyRequestFailureInTheMiddleOfProxyingBigContent() throws Exception |
| { |
| int outputBufferSize = 1024; |
| final CountDownLatch chunk1Latch = new CountDownLatch(1); |
| final byte[] chunk1 = new byte[outputBufferSize]; |
| new Random().nextBytes(chunk1); |
| final int chunk2 = 'w'; |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException |
| { |
| ServletOutputStream output = response.getOutputStream(); |
| output.write(chunk1); |
| response.flushBuffer(); |
| |
| // Wait for the client to receive this chunk. |
| await(chunk1Latch, 5000); |
| |
| // Send second chunk, must not be received by proxy. |
| output.write(chunk2); |
| } |
| |
| private boolean await(CountDownLatch latch, long ms) throws IOException |
| { |
| try |
| { |
| return latch.await(ms, TimeUnit.MILLISECONDS); |
| } |
| catch (InterruptedException x) |
| { |
| throw new InterruptedIOException(); |
| } |
| } |
| }); |
| final long proxyTimeout = 1000; |
| Map<String, String> proxyParams = new HashMap<>(); |
| proxyParams.put("timeout", String.valueOf(proxyTimeout)); |
| proxyParams.put("outputBufferSize", String.valueOf(outputBufferSize)); |
| startProxy(proxyParams); |
| startClient(); |
| |
| InputStreamResponseListener listener = new InputStreamResponseListener(); |
| int port = serverConnector.getLocalPort(); |
| client.newRequest("localhost", port).send(listener); |
| |
| Response response = listener.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(200, response.getStatus()); |
| |
| InputStream input = listener.getInputStream(); |
| for (int i = 0; i < chunk1.length; ++i) |
| Assert.assertEquals(chunk1[i] & 0xFF, input.read()); |
| |
| TimeUnit.MILLISECONDS.sleep(2 * proxyTimeout); |
| |
| chunk1Latch.countDown(); |
| |
| try |
| { |
| // Make sure the proxy does not receive chunk2. |
| input.read(); |
| Assert.fail(); |
| } |
| catch (EOFException x) |
| { |
| // Expected |
| } |
| |
| HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); |
| Assert.assertEquals(0, destination.getConnectionPool().getIdleConnections().size()); |
| } |
| |
| @Test |
| public void testResponseHeadersAreNotRemoved() throws Exception |
| { |
| startServer(new EmptyHttpServlet()); |
| startProxy(); |
| proxyContext.stop(); |
| final String headerName = "X-Test"; |
| final String headerValue = "test-value"; |
| proxyContext.addFilter(new FilterHolder(new Filter() |
| { |
| @Override |
| public void init(FilterConfig filterConfig) throws ServletException |
| { |
| } |
| |
| @Override |
| public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException |
| { |
| ((HttpServletResponse)response).addHeader(headerName, headerValue); |
| chain.doFilter(request, response); |
| } |
| |
| @Override |
| public void destroy() |
| { |
| } |
| }), "/*", EnumSet.of(DispatcherType.REQUEST)); |
| proxyContext.start(); |
| startClient(); |
| |
| ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertEquals(headerValue, response.getHeaders().get(headerName)); |
| } |
| |
| @Test |
| public void testHeadersListedByConnectionHeaderAreRemoved() throws Exception |
| { |
| final Map<String, String> hopHeaders = new LinkedHashMap<>(); |
| hopHeaders.put(HttpHeader.TE.asString(), "gzip"); |
| hopHeaders.put(HttpHeader.CONNECTION.asString(), "Keep-Alive, Foo, Bar"); |
| hopHeaders.put("Foo", "abc"); |
| hopHeaders.put("Foo", "def"); |
| hopHeaders.put(HttpHeader.KEEP_ALIVE.asString(), "timeout=30"); |
| startServer(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException |
| { |
| List<String> names = Collections.list(request.getHeaderNames()); |
| for (String name : names) |
| { |
| if (hopHeaders.containsKey(name)) |
| throw new IOException("Hop header must not be proxied: " + name); |
| } |
| } |
| }); |
| startProxy(); |
| startClient(); |
| |
| Request request = client.newRequest("localhost", serverConnector.getLocalPort()); |
| for (Map.Entry<String, String> entry : hopHeaders.entrySet()) |
| request.header(entry.getKey(), entry.getValue()); |
| ContentResponse response = request |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| |
| Assert.assertEquals(200, response.getStatus()); |
| } |
| |
| // TODO: test proxy authentication |
| } |