diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java index 46995c3f3..d264e7239 100644 --- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java +++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java @@ -232,7 +232,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, this.serverResolver = serverResolver; ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(ServerGroup.INCOMING_WORKER_THREADS); - this.globalTrafficShapingHandler = new GlobalTrafficShapingHandler(executor, writeThrottleBytesPerSecond, readThrottleBytesPerSecond, 250L); + this.globalTrafficShapingHandler = new GlobalTrafficShapingHandler(executor, writeThrottleBytesPerSecond, readThrottleBytesPerSecond, 250L, Long.MAX_VALUE); } boolean isTransparent() { diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index 34f231b44..87f5aac69 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -763,20 +763,6 @@ private void initChannelPipeline(ChannelPipeline pipeline, HttpRequest httpRequest) { if (trafficHandler != null) { - if (trafficHandler.getReadLimit() > 0) { - long initial = trafficHandler.getReadLimit() / 4; - if (initial < MINIMUM_RECV_BUFFER_SIZE_BYTES) { - initial = MINIMUM_RECV_BUFFER_SIZE_BYTES; - } - - long max = trafficHandler.getReadLimit() / 2; - if (max < MINIMUM_RECV_BUFFER_SIZE_BYTES) { - max = MINIMUM_RECV_BUFFER_SIZE_BYTES; - } - - AdaptiveRecvByteBufAllocator adaptiveRecvByteBufAllocator = new AdaptiveRecvByteBufAllocator(MINIMUM_RECV_BUFFER_SIZE_BYTES, (int)initial, (int)max); - pipeline.channel().config().setRecvByteBufAllocator(adaptiveRecvByteBufAllocator); - } pipeline.addLast("global-traffic-shaping", trafficHandler); } diff --git a/src/test/java/org/littleshoot/proxy/TestUtils.java b/src/test/java/org/littleshoot/proxy/TestUtils.java index d7d32ea9f..8e2e5f295 100644 --- a/src/test/java/org/littleshoot/proxy/TestUtils.java +++ b/src/test/java/org/littleshoot/proxy/TestUtils.java @@ -54,6 +54,20 @@ public static Server startWebServer(final int port) throws Exception { return startWebServer(port, null); } + /** + * Creates and starts embedded web server that is running on given port. + * Each response has a body that contains the specified contents. + * + * @param port + * The port + * @return Instance of Server + * @throws Exception + * if failed to start + */ + public static Server startWebServerWithResponse(final int port, byte[] content) throws Exception { + return startWebServerWithResponse(port, null, content); + } + /** * Creates and starts embedded web server that is running on given port, * including an SSL connector on the other given port. Each response has a @@ -130,6 +144,83 @@ public void handle(String target, Request baseRequest, return httpServer; } + /** + * Creates and starts embedded web server that is running on given port, + * including an SSL connector on the other given port. Each response has a + * body that contains the specified contents. + * + * @param port + * The port + * @param sslPort + * (optional) The ssl port + * @param content + * The response the server will return + * @return Instance of Server + * @throws Exception + * if failed to start + */ + public static Server startWebServerWithResponse(final int port, final Integer sslPort, final byte[] content) + throws Exception { + final Server httpServer = new Server(port); + httpServer.setHandler(new AbstractHandler() { + public void handle(String target, Request baseRequest, + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + if (request.getRequestURI().contains("hang")) { + System.out.println("Hanging as requested"); + try { + Thread.sleep(90000); + } catch (InterruptedException ie) { + System.out.println("Stopped hanging due to interruption"); + } + } + + long numberOfBytesRead = 0; + InputStream in = new BufferedInputStream(request + .getInputStream()); + while (in.read() != -1) { + numberOfBytesRead += 1; + } + System.out.println("Done reading # of bytes: " + + numberOfBytesRead); + response.setStatus(HttpServletResponse.SC_OK); + baseRequest.setHandled(true); + + response.addHeader("Content-Length", Integer.toString(content.length)); + response.getOutputStream().write(content); + } + }); + if (sslPort != null) { + // Add SSL connector + org.eclipse.jetty.util.ssl.SslContextFactory sslContextFactory = new org.eclipse.jetty.util.ssl.SslContextFactory(); + + SelfSignedSslEngineSource contextSource = new SelfSignedSslEngineSource(); + SSLContext sslContext = contextSource.getSslContext(); + + sslContextFactory.setSslContext(sslContext); + SslSocketConnector connector = new SslSocketConnector( + sslContextFactory); + connector.setPort(sslPort); + /* + *
Ox: For some reason, on OS X, a non-zero timeout can causes + * sporadic issues. This + * StackOverflow thread has some insights into it, but I don't + * quite get it.
+ * + *This can cause problems with Jetty's SSL handshaking, so I + * have to set the handshake timeout and the maxIdleTime to 0 so + * that the SSLSocket has an infinite timeout.
+ */ + connector.setHandshakeTimeout(0); + connector.setMaxIdleTime(0); + httpServer.addConnector(connector); + } + httpServer.start(); + return httpServer; + } + /** * Creates instance HttpClient that is configured to use proxy server. The * proxy server should run on 127.0.0.1 and given port diff --git a/src/test/java/org/littleshoot/proxy/ThrottlingTest.java b/src/test/java/org/littleshoot/proxy/ThrottlingTest.java new file mode 100644 index 000000000..567e1748d --- /dev/null +++ b/src/test/java/org/littleshoot/proxy/ThrottlingTest.java @@ -0,0 +1,207 @@ +package org.littleshoot.proxy; + +import org.apache.http.HttpHost; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.params.ConnRoutePNames; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.CoreConnectionPNames; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.server.Server; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +public class ThrottlingTest { + + private static final int WRITE_WEB_SERVER_PORT = 8926; + private static final int READ_WEB_SERVER_PORT = 8927; + private static final long THROTTLED_READ_BYTES_PER_SECOND = 25000L; + private static final long THROTTLED_WRITE_BYTES_PER_SECOND = 25000L; + + // throttling is not guaranteed to be exact, so allow some variation in the amount of time the call takes. since we want + // these tests to take just a few seconds, allow significant variation. even with this large variation, if throttling + // is broken it should take much less time than expected. + private static final double ALLOWABLE_VARIATION = 0.30; + + private Server writeWebServer; + private Server readWebServer; + + private byte[] largeData; + + private int msToWriteThrottled; + private int msToReadThrottled; + + // time to allow for an unthrottled local request + private static final int UNTRHOTTLED_REQUEST_TIME_MS = 1000; + + @Before + public void setUp() throws Exception { + // Set up some large data + largeData = new byte[100000]; + for (int i = 0; i < largeData.length; i++) { + largeData[i] = 1 % 256; + } + + msToWriteThrottled = largeData.length * 1000 / (int)THROTTLED_WRITE_BYTES_PER_SECOND; + msToReadThrottled = largeData.length * 1000 / (int)THROTTLED_READ_BYTES_PER_SECOND; + + writeWebServer = TestUtils.startWebServer(WRITE_WEB_SERVER_PORT); + readWebServer = TestUtils.startWebServerWithResponse(READ_WEB_SERVER_PORT, largeData); + } + + @After + public void tearDown() throws Exception { + writeWebServer.stop(); + readWebServer.stop(); + } + + @Test + public void testThrottledWrite() throws Exception { + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withPort(0) + .withThrottling(0, THROTTLED_WRITE_BYTES_PER_SECOND) + .start(); + + int proxyPort = proxyServer.getListenAddress().getPort(); + + final HttpPost request = new HttpPost("/"); + request.getParams().setParameter( + CoreConnectionPNames.CONNECTION_TIMEOUT, 5000); + final ByteArrayEntity entity = new ByteArrayEntity(largeData); + entity.setChunked(true); + request.setEntity(entity); + + DefaultHttpClient httpClient = new DefaultHttpClient(); + final HttpHost proxy = new HttpHost("127.0.0.1", proxyPort, "http"); + httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, + proxy); + + long start = System.currentTimeMillis(); + final org.apache.http.HttpResponse response = httpClient.execute( + new HttpHost("127.0.0.1", + WRITE_WEB_SERVER_PORT), request); + long finish = System.currentTimeMillis(); + + Assert.assertEquals("Received " + largeData.length + " bytes\n", + EntityUtils.toString(response.getEntity())); + + Assert.assertTrue("Expected throttled write to complete in approximately " + msToWriteThrottled + "ms" + " but took " + (finish - start) + "ms", + finish - start > msToWriteThrottled * (1 - ALLOWABLE_VARIATION) + && (finish - start < msToWriteThrottled * (1 + ALLOWABLE_VARIATION))); + + proxyServer.stop(); + } + + @Test + public void testUnthrottledWrite() throws Exception { + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withPort(0) + .start(); + + int proxyPort = proxyServer.getListenAddress().getPort(); + + final HttpPost request = new HttpPost("/"); + request.getParams().setParameter( + CoreConnectionPNames.CONNECTION_TIMEOUT, 5000); + final ByteArrayEntity entity = new ByteArrayEntity(largeData); + entity.setChunked(true); + request.setEntity(entity); + + DefaultHttpClient httpClient = new DefaultHttpClient(); + final HttpHost proxy = new HttpHost("127.0.0.1", proxyPort, "http"); + httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, + proxy); + + long start = System.currentTimeMillis(); + final org.apache.http.HttpResponse response = httpClient.execute( + new HttpHost("127.0.0.1", + WRITE_WEB_SERVER_PORT), request); + long finish = System.currentTimeMillis(); + + Assert.assertEquals("Received " + largeData.length + " bytes\n", + EntityUtils.toString(response.getEntity())); + + Assert.assertTrue("Unthrottled write took " + (finish - start) + "ms, but expected to complete in " + UNTRHOTTLED_REQUEST_TIME_MS + "ms", finish - start < UNTRHOTTLED_REQUEST_TIME_MS); + + proxyServer.stop(); + } + + @Test + public void testThrottledRead() throws Exception { + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withPort(0) + .withThrottling(THROTTLED_READ_BYTES_PER_SECOND, 0) + .start(); + + int proxyPort = proxyServer.getListenAddress().getPort(); + + final HttpGet request = new HttpGet("/"); + request.getParams().setParameter( + CoreConnectionPNames.CONNECTION_TIMEOUT, 5000); + + DefaultHttpClient httpClient = new DefaultHttpClient(); + final HttpHost proxy = new HttpHost("127.0.0.1", proxyPort, "http"); + httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, + proxy); + + long start = System.currentTimeMillis(); + final org.apache.http.HttpResponse response = httpClient.execute( + new HttpHost("127.0.0.1", + READ_WEB_SERVER_PORT), request); + byte[] readContent = new byte[100000]; + + int bytesRead = 0; + while (bytesRead < largeData.length) { + int read = response.getEntity().getContent().read(readContent, bytesRead, largeData.length - bytesRead); + bytesRead += read; + } + + long finish = System.currentTimeMillis(); + + Assert.assertTrue("Expected throttled read to complete in approximately " + msToReadThrottled + "ms" + " but took " + (finish - start) + "ms", + finish - start > msToReadThrottled * (1 - ALLOWABLE_VARIATION) + && (finish - start < msToReadThrottled * (1 + ALLOWABLE_VARIATION))); + + proxyServer.stop(); + } + + @Test + public void testUnthrottledRead() throws Exception { + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withPort(0) + .start(); + + int proxyPort = proxyServer.getListenAddress().getPort(); + + final HttpGet request = new HttpGet("/"); + request.getParams().setParameter( + CoreConnectionPNames.CONNECTION_TIMEOUT, 5000); + + DefaultHttpClient httpClient = new DefaultHttpClient(); + final HttpHost proxy = new HttpHost("127.0.0.1", proxyPort, "http"); + httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, + proxy); + + long start = System.currentTimeMillis(); + final org.apache.http.HttpResponse response = httpClient.execute( + new HttpHost("127.0.0.1", + READ_WEB_SERVER_PORT), request); + + byte[] readContent = new byte[100000]; + int bytesRead = 0; + while (bytesRead < largeData.length) { + int read = response.getEntity().getContent().read(readContent, bytesRead, largeData.length - bytesRead); + bytesRead += read; + } + + long finish = System.currentTimeMillis(); + + Assert.assertTrue("Unthrottled read took " + (finish - start) + "ms, but expected to complete in " + UNTRHOTTLED_REQUEST_TIME_MS + "ms", finish - start < UNTRHOTTLED_REQUEST_TIME_MS); + + proxyServer.stop(); + } +}