diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 5592dd13bccd..7a3a429d04b3 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -135,6 +135,7 @@ public class HttpClient extends ContainerLifeCycle implements AutoCloseable private String defaultRequestContentType = "application/octet-stream"; private boolean useInputDirectByteBuffers = true; private boolean useOutputDirectByteBuffers = true; + private int maxRequestHeadersSize = 32 * 1024; private int maxResponseHeadersSize = -1; private Sweeper destinationSweeper; @@ -1142,6 +1143,14 @@ public ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.C return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory); } + public int getMaxRequestHeadersSize() { + return maxRequestHeadersSize; + } + + public void setMaxRequestHeadersSize(int maxRequestHeadersSize) { + this.maxRequestHeadersSize = maxRequestHeadersSize; + } + @Override public void close() throws Exception { diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpSenderOverHTTP.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpSenderOverHTTP.java index 1f6bc4a3f756..b1d26e64ff5c 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpSenderOverHTTP.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpSenderOverHTTP.java @@ -156,6 +156,7 @@ protected Action process() throws Exception { HttpClient httpClient = getHttpChannel().getHttpDestination().getHttpClient(); HttpExchange exchange = getHttpExchange(); + ByteBufferPool bufferPool = httpClient.getByteBufferPool(); boolean useDirectByteBuffers = httpClient.isUseOutputDirectByteBuffers(); while (true) @@ -178,9 +179,21 @@ protected Action process() throws Exception } case HEADER_OVERFLOW: { - headerBuffer.release(); - headerBuffer = null; - throw new IllegalArgumentException("Request header too large"); + int maxRequestHeadersSize = httpClient.getMaxRequestHeadersSize(); + if (headerBuffer.capacity() < maxRequestHeadersSize) { + RetainableByteBuffer newHeaderBuffer = bufferPool.acquire(maxRequestHeadersSize, useDirectByteBuffers); + headerBuffer.getByteBuffer().flip(); + newHeaderBuffer.getByteBuffer().put(headerBuffer.getByteBuffer()); + RetainableByteBuffer toRelease = headerBuffer; + headerBuffer = newHeaderBuffer; + toRelease.release(); + break; + } + else { + headerBuffer.release(); + headerBuffer = null; + throw new IllegalArgumentException("Request header too large"); + } } case NEED_CHUNK: { diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java index 313fd841de41..c7101f466a59 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java @@ -16,7 +16,10 @@ import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -29,6 +32,7 @@ import org.eclipse.jetty.client.Result; import org.eclipse.jetty.client.transport.HttpDestination; import org.eclipse.jetty.client.transport.internal.HttpConnectionOverHTTP; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.io.ByteArrayEndPoint; import org.eclipse.jetty.util.Promise; import org.hamcrest.Matchers; @@ -39,6 +43,7 @@ import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.wildfly.common.Assert.assertFalse; public class HttpSenderOverHTTPTest { @@ -302,4 +307,187 @@ public void onSuccess(Request request) assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); assertTrue(successLatch.await(5, TimeUnit.SECONDS)); } + + private static Random rnd = new Random(); + private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + + public static final int CHARS_LENGTH = CHARS.length(); + + protected static String getRandomString(int size) { + StringBuilder sb = new StringBuilder(size); + while (sb.length() < size) { // length of the random string. + int index = rnd.nextInt(CHARS_LENGTH); + sb.append(CHARS.charAt(index)); + } + return sb.toString(); + } + + @Test + public void testSmallHeadersSize() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); + destination.start(); + HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); + Request request = client.newRequest(URI.create("http://localhost/")); + request.agent(getRandomString(888)); //More than the request buffer size, but less than the default max request headers size + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch successLatch = new CountDownLatch(1); + final CountDownLatch failureLatch = new CountDownLatch(1); + request.listener(new Request.Listener() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onSuccess(Request request) + { + successLatch.countDown(); + } + + @Override + public void onFailure(Request request, Throwable failure) { + failureLatch.countDown(); + } + }); + connection.send(request, null); + + String requestString = endPoint.takeOutputString(); + assertTrue(requestString.startsWith("GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\n")); + assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + assertTrue(successLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testMaxRequestHeadersSize() throws Exception + { + byte[] buffer = new byte[32 * 1024]; + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(buffer, buffer.length); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); + destination.start(); + HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); + Request request = client.newRequest(URI.create("http://localhost/")); + //More than the request buffer size, but less than the default max request headers size + + int desiredHeadersSize = 20 * 1024; + int currentHeadersSize = 0; + int i = 0; + while(currentHeadersSize < desiredHeadersSize) { + final int index = i ++; + final String headerValue = getRandomString(800); + final int headerSize = headerValue.length(); + currentHeadersSize += headerSize; + request.cookie(new HttpCookie() { + @Override + public String getName() { + return "large" + index; + } + + @Override + public String getValue() { + return headerValue; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public Map getAttributes() { + return new HashMap<>(); + } + }); + } + + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch successLatch = new CountDownLatch(1); + request.listener(new Request.Listener() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onSuccess(Request request) + { + successLatch.countDown(); + } + }); + connection.send(request, null); + + String requestString = endPoint.takeOutputString(); + assertTrue(requestString.startsWith("GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\n")); + assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + assertTrue(successLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testMaxRequestHeadersSizeOverflow() throws Exception + { + byte[] buffer = new byte[32 * 1024]; + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(buffer, buffer.length); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); + destination.start(); + HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); + Request request = client.newRequest(URI.create("http://localhost/")); + //More than the request buffer size, but less than the default max request headers size + + int desiredHeadersSize = 35 * 1024; + int currentHeadersSize = 0; + int i = 0; + while(currentHeadersSize < desiredHeadersSize) { + final int index = i ++; + final String headerValue = getRandomString(800); + final int headerSize = headerValue.length(); + currentHeadersSize += headerSize; + request.cookie(new HttpCookie() { + @Override + public String getName() { + return "large" + index; + } + + @Override + public String getValue() { + return headerValue; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public Map getAttributes() { + return new HashMap<>(); + } + }); + } + + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch failureLatch = new CountDownLatch(1); + request.listener(new Request.Listener() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onFailure(Request request, Throwable failure) + { + failureLatch.countDown(); + } + }); + connection.send(request, null); + + assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + } }