diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index d54ef0e832..fc53497c3a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -69,6 +69,8 @@ import org.springframework.util.unit.DataSize; * @author Victor Mandujano * @author Chris Bono * @author Parviz Rozikov + * @author Florian Storz + * @author Michael Weidmann * @since 1.0.0 */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @@ -490,6 +492,11 @@ public class ServerProperties { */ private final Remoteip remoteip = new Remoteip(); + /** + * Maximum size of the HTTP response header. + */ + private DataSize maxHttpResponseHeaderSize = DataSize.ofKilobytes(8); + public DataSize getMaxHttpFormPostSize() { return this.maxHttpFormPostSize; } @@ -646,6 +653,14 @@ public class ServerProperties { return this.remoteip; } + public DataSize getMaxHttpResponseHeaderSize() { + return maxHttpResponseHeaderSize; + } + + public void setMaxHttpResponseHeaderSize(DataSize maxHttpResponseHeaderSize) { + this.maxHttpResponseHeaderSize = maxHttpResponseHeaderSize; + } + /** * Tomcat access log properties. */ @@ -1096,6 +1111,11 @@ public class ServerProperties { */ private Duration connectionIdleTimeout; + /** + * Maximum size of the HTTP response header. + */ + private DataSize maxHttpResponseHeaderSize = DataSize.ofKilobytes(8); + public Accesslog getAccesslog() { return this.accesslog; } @@ -1120,6 +1140,14 @@ public class ServerProperties { this.connectionIdleTimeout = connectionIdleTimeout; } + public DataSize getMaxHttpResponseHeaderSize() { + return maxHttpResponseHeaderSize; + } + + public void setMaxHttpResponseHeaderSize(DataSize maxHttpResponseHeaderSize) { + this.maxHttpResponseHeaderSize = maxHttpResponseHeaderSize; + } + /** * Jetty access log properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index 709c908c10..f950f7c2fe 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java @@ -53,6 +53,8 @@ import org.springframework.util.unit.DataSize; * @author Phillip Webb * @author HaiTao Zhang * @author Rafiullah Hamedy + * @author Florian Storz + * @author Michael Weidmann * @since 2.0.0 */ public class JettyWebServerFactoryCustomizer @@ -85,6 +87,9 @@ public class JettyWebServerFactoryCustomizer propertyMapper.from(properties::getMaxHttpRequestHeaderSize).whenNonNull().asInt(DataSize::toBytes) .when(this::isPositive).to((maxHttpRequestHeaderSize) -> factory .addServerCustomizers(new MaxHttpRequestHeaderSizeCustomizer(maxHttpRequestHeaderSize))); + propertyMapper.from(jettyProperties::getMaxHttpResponseHeaderSize).whenNonNull().asInt(DataSize::toBytes) + .when(this::isPositive).to((maxHttpResponseHeaderSize) -> factory + .addServerCustomizers(new MaxHttpResponseHeaderSizeCustomizer(maxHttpResponseHeaderSize))); propertyMapper.from(jettyProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes).when(this::isPositive) .to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); propertyMapper.from(jettyProperties::getConnectionIdleTimeout).whenNonNull() @@ -192,14 +197,27 @@ public class JettyWebServerFactoryCustomizer return CustomRequestLog.NCSA_FORMAT; } - private static class MaxHttpRequestHeaderSizeCustomizer implements JettyServerCustomizer { + private record MaxHttpRequestHeaderSizeCustomizer(int maxRequestHeaderSize) implements JettyServerCustomizer { - private final int maxRequestHeaderSize; + @Override + public void customize(Server server) { + Arrays.stream(server.getConnectors()).forEach(this::customize); + } - MaxHttpRequestHeaderSizeCustomizer(int maxRequestHeaderSize) { - this.maxRequestHeaderSize = maxRequestHeaderSize; + private void customize(org.eclipse.jetty.server.Connector connector) { + connector.getConnectionFactories().forEach(this::customize); } + private void customize(ConnectionFactory factory) { + if (factory instanceof HttpConfiguration.ConnectionFactory) { + ((HttpConfiguration.ConnectionFactory) factory).getHttpConfiguration() + .setRequestHeaderSize(this.maxRequestHeaderSize); + } + } + } + + private record MaxHttpResponseHeaderSizeCustomizer(int maxResponseHeaderSize) implements JettyServerCustomizer { + @Override public void customize(Server server) { Arrays.stream(server.getConnectors()).forEach(this::customize); @@ -212,10 +230,9 @@ public class JettyWebServerFactoryCustomizer private void customize(ConnectionFactory factory) { if (factory instanceof HttpConfiguration.ConnectionFactory) { ((HttpConfiguration.ConnectionFactory) factory).getHttpConfiguration() - .setRequestHeaderSize(this.maxRequestHeaderSize); + .setResponseHeaderSize(this.maxResponseHeaderSize); } } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 0a5fb74756..56045d5f93 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -16,10 +16,6 @@ package org.springframework.boot.autoconfigure.web.embedded; -import java.time.Duration; -import java.util.List; -import java.util.stream.Collectors; - import org.apache.catalina.Lifecycle; import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.ErrorReportValve; @@ -29,7 +25,6 @@ import org.apache.coyote.ProtocolHandler; import org.apache.coyote.UpgradeProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.coyote.http2.Http2Protocol; - import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeAttribute; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -44,6 +39,11 @@ import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; import org.springframework.util.unit.DataSize; +import java.time.Duration; +import java.util.List; +import java.util.function.ObjIntConsumer; +import java.util.stream.Collectors; + /** * Customization for Tomcat-specific features common for both Servlet and Reactive * servers. @@ -59,6 +59,8 @@ import org.springframework.util.unit.DataSize; * @author Rafiullah Hamedy * @author Victor Mandujano * @author Parviz Rozikov + * @author Florian Storz + * @author Michael Weidmann * @since 2.0.0 */ public class TomcatWebServerFactoryCustomizer @@ -95,6 +97,9 @@ public class TomcatWebServerFactoryCustomizer propertyMapper.from(this.serverProperties.getMaxHttpRequestHeaderSize()).whenNonNull().asInt(DataSize::toBytes) .when(this::isPositive) .to((maxHttpRequestHeaderSize) -> customizeMaxHttpRequestHeaderSize(factory, maxHttpRequestHeaderSize)); + propertyMapper.from(tomcatProperties::getMaxHttpResponseHeaderSize).whenNonNull().asInt(DataSize::toBytes) + .when(this::isPositive).to((maxHttpResponseHeaderSize) -> customizeMaxHttpResponseHeaderSize(factory, + maxHttpResponseHeaderSize)); propertyMapper.from(tomcatProperties::getMaxSwallowSize).whenNonNull().asInt(DataSize::toBytes) .to((maxSwallowSize) -> customizeMaxSwallowSize(factory, maxSwallowSize)); propertyMapper.from(tomcatProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes) @@ -129,22 +134,14 @@ public class TomcatWebServerFactoryCustomizer return value > 0; } + @SuppressWarnings("rawtypes") private void customizeAcceptCount(ConfigurableTomcatWebServerFactory factory, int acceptCount) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractProtocol protocol) { - protocol.setAcceptCount(acceptCount); - } - }); + customizeHandler(factory, acceptCount, AbstractProtocol.class, AbstractProtocol::setAcceptCount); } + @SuppressWarnings("rawtypes") private void customizeProcessorCache(ConfigurableTomcatWebServerFactory factory, int processorCache) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractProtocol) { - ((AbstractProtocol) handler).setProcessorCache(processorCache); - } - }); + customizeHandler(factory, processorCache, AbstractProtocol.class, AbstractProtocol::setProcessorCache); } private void customizeKeepAliveTimeout(ConfigurableTomcatWebServerFactory factory, Duration keepAliveTimeout) { @@ -161,31 +158,21 @@ public class TomcatWebServerFactoryCustomizer }); } + @SuppressWarnings("rawtypes") private void customizeMaxKeepAliveRequests(ConfigurableTomcatWebServerFactory factory, int maxKeepAliveRequests) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractHttp11Protocol protocol) { - protocol.setMaxKeepAliveRequests(maxKeepAliveRequests); - } - }); + customizeHandler(factory, maxKeepAliveRequests, AbstractHttp11Protocol.class, + AbstractHttp11Protocol::setMaxKeepAliveRequests); } + @SuppressWarnings("rawtypes") private void customizeMaxConnections(ConfigurableTomcatWebServerFactory factory, int maxConnections) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractProtocol protocol) { - protocol.setMaxConnections(maxConnections); - } - }); + customizeHandler(factory, maxConnections, AbstractProtocol.class, AbstractProtocol::setMaxConnections); } + @SuppressWarnings("rawtypes") private void customizeConnectionTimeout(ConfigurableTomcatWebServerFactory factory, Duration connectionTimeout) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractProtocol protocol) { - protocol.setConnectionTimeout((int) connectionTimeout.toMillis()); - } - }); + customizeHandler(factory, (int) connectionTimeout.toMillis(), AbstractProtocol.class, + AbstractProtocol::setConnectionTimeout); } private void customizeRelaxedPathChars(ConfigurableTomcatWebServerFactory factory, String relaxedChars) { @@ -248,40 +235,40 @@ public class TomcatWebServerFactoryCustomizer @SuppressWarnings("rawtypes") private void customizeMaxThreads(ConfigurableTomcatWebServerFactory factory, int maxThreads) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractProtocol protocol) { - protocol.setMaxThreads(maxThreads); - } - }); + customizeHandler(factory, maxThreads, AbstractProtocol.class, AbstractProtocol::setMaxThreads); } @SuppressWarnings("rawtypes") private void customizeMinThreads(ConfigurableTomcatWebServerFactory factory, int minSpareThreads) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractProtocol protocol) { - protocol.setMinSpareThreads(minSpareThreads); - } - }); + customizeHandler(factory, minSpareThreads, AbstractProtocol.class, AbstractProtocol::setMinSpareThreads); } @SuppressWarnings("rawtypes") private void customizeMaxHttpRequestHeaderSize(ConfigurableTomcatWebServerFactory factory, int maxHttpRequestHeaderSize) { - factory.addConnectorCustomizers((connector) -> { - ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractHttp11Protocol protocol) { - protocol.setMaxHttpRequestHeaderSize(maxHttpRequestHeaderSize); - } - }); + customizeHandler(factory, maxHttpRequestHeaderSize, AbstractHttp11Protocol.class, + AbstractHttp11Protocol::setMaxHttpRequestHeaderSize); } + @SuppressWarnings("rawtypes") + private void customizeMaxHttpResponseHeaderSize(ConfigurableTomcatWebServerFactory factory, + int maxHttpResponseHeaderSize) { + customizeHandler(factory, maxHttpResponseHeaderSize, AbstractHttp11Protocol.class, + AbstractHttp11Protocol::setMaxHttpResponseHeaderSize); + } + + @SuppressWarnings("rawtypes") private void customizeMaxSwallowSize(ConfigurableTomcatWebServerFactory factory, int maxSwallowSize) { + customizeHandler(factory, maxSwallowSize, AbstractHttp11Protocol.class, + AbstractHttp11Protocol::setMaxSwallowSize); + } + + private void customizeHandler(ConfigurableTomcatWebServerFactory factory, int value, + Class type, ObjIntConsumer consumer) { factory.addConnectorCustomizers((connector) -> { ProtocolHandler handler = connector.getProtocolHandler(); - if (handler instanceof AbstractHttp11Protocol protocol) { - protocol.setMaxSwallowSize(maxSwallowSize); + if (type.isAssignableFrom(handler.getClass())) { + consumer.accept(type.cast(handler), value); } }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java index 47aa183033..1c098a01ae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java @@ -16,15 +16,6 @@ package org.springframework.boot.autoconfigure.web.embedded; -import java.io.File; -import java.io.IOException; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.SynchronousQueue; - import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.CustomRequestLog; @@ -38,7 +29,6 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties.ForwardHeadersStrategy; import org.springframework.boot.autoconfigure.web.ServerProperties.Jetty; @@ -53,6 +43,17 @@ import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.unit.DataSize; + +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; @@ -286,6 +287,61 @@ class JettyWebServerFactoryCustomizerTests { assertThat(requestHeaderSizes).containsOnly(8192); } + @Test + void customizeMaxRequestHttpHeaderSize() { + bind("server.max-http-request-header-size=2048"); + JettyWebServer server = customizeAndGetServer(); + List requestHeaderSizes = getRequestHeaderSizes(server); + assertThat(requestHeaderSizes).containsOnly(2048); + } + + @Test + void customMaxHttpRequestHeaderSizeIgnoredIfNegative() { + bind("server.max-http-request-header-size=-1"); + JettyWebServer server = customizeAndGetServer(); + List requestHeaderSizes = getRequestHeaderSizes(server); + assertThat(requestHeaderSizes).containsOnly(8192); + } + + @Test + void customMaxHttpRequestHeaderSizeIgnoredIfZero() { + bind("server.max-http-request-header-size=0"); + JettyWebServer server = customizeAndGetServer(); + List requestHeaderSizes = getRequestHeaderSizes(server); + assertThat(requestHeaderSizes).containsOnly(8192); + } + + @Test + void defaultMaxHttpResponseHeaderSize() { + JettyWebServer server = customizeAndGetServer(); + List responseHeaderSizes = getResponseHeaderSizes(server); + assertThat(responseHeaderSizes).containsOnly(8192); + } + + @Test + void customizeMaxHttpResponseHeaderSize() { + bind("server.jetty.max-http-response-header-size=2KB"); + JettyWebServer server = customizeAndGetServer(); + List responseHeaderSizes = getResponseHeaderSizes(server); + assertThat(responseHeaderSizes).containsOnly(2048); + } + + @Test + void customMaxHttpResponseHeaderSizeIgnoredIfNegative() { + bind("server.jetty.max-http-response-header-size=-1"); + JettyWebServer server = customizeAndGetServer(); + List responseHeaderSizes = getResponseHeaderSizes(server); + assertThat(responseHeaderSizes).containsOnly(8192); + } + + @Test + void customMaxHttpResponseHeaderSizeIgnoredIfZero() { + bind("server.jetty.max-http-response-header-size=0"); + JettyWebServer server = customizeAndGetServer(); + List responseHeaderSizes = getResponseHeaderSizes(server); + assertThat(responseHeaderSizes).containsOnly(8192); + } + @Test void customIdleTimeout() { bind("server.jetty.connection-idle-timeout=60s"); @@ -303,6 +359,14 @@ class JettyWebServerFactoryCustomizerTests { } private List getRequestHeaderSizes(JettyWebServer server) { + return getHeaderSizes(server, HttpConfiguration::getRequestHeaderSize); + } + + private List getResponseHeaderSizes(JettyWebServer server) { + return getHeaderSizes(server, HttpConfiguration::getResponseHeaderSize); + } + + private List getHeaderSizes(JettyWebServer server, Function provider) { List requestHeaderSizes = new ArrayList<>(); // Start (and directly stop) server to have connectors available server.start(); @@ -313,7 +377,7 @@ class JettyWebServerFactoryCustomizerTests { .forEach((cf) -> { ConnectionFactory factory = (ConnectionFactory) cf; HttpConfiguration configuration = factory.getHttpConfiguration(); - requestHeaderSizes.add(configuration.getRequestHeaderSize()); + requestHeaderSizes.add(provider.apply(configuration)); }); } return requestHeaderSizes; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java index 3bd0407e2c..3da00687dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java @@ -211,6 +211,68 @@ class TomcatWebServerFactoryCustomizerTests { .getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); } + @Test + void defaultMaxHttpRequestHeaderSize() { + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); + } + + @Test + void customMaxHttpRequestHeaderSize() { + bind("server.max-http-request-header-size=10MB"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofMegabytes(10).toBytes())); + } + + @Test + void customMaxRequestHttpHeaderSizeIgnoredIfNegative() { + bind("server.max-http-request-header-size=-1"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); + } + + @Test + void customMaxRequestHttpHeaderSizeIgnoredIfZero() { + bind("server.max-http-request-header-size=0"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); + } + + @Test + void defaultMaxHttpResponseHeaderSize() { + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpResponseHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); + } + + @Test + void customMaxHttpResponseHeaderSize() { + bind("server.tomcat.max-http-response-header-size=10MB"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpResponseHeaderSize()).isEqualTo(DataSize.ofMegabytes(10).toBytes())); + } + + @Test + void customMaxResponseHttpHeaderSizeIgnoredIfNegative() { + bind("server.tomcat.max-http-response-header-size=-1"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpResponseHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); + } + + @Test + void customMaxResponseHttpHeaderSizeIgnoredIfZero() { + bind("server.tomcat.max-http-response-header-size=0"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxHttpResponseHeaderSize()).isEqualTo(DataSize.ofKilobytes(8).toBytes())); + } + @Test void customMaxSwallowSize() { bind("server.tomcat.max-swallow-size=10MB"); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/service/HttpHeaderService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/service/HttpHeaderService.java new file mode 100644 index 0000000000..f4f9e321a4 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/service/HttpHeaderService.java @@ -0,0 +1,26 @@ +package smoketest.jetty.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import smoketest.jetty.util.RandomStringUtil; + +@Component +public class HttpHeaderService { + + @Value("${server.jetty.max-http-response-header-size}") + private int maxHttpResponseHeaderSize; + + /** + * generate a random byte array that + *
    + *
  1. is longer than configured + * server.jetty.max-http-response-header-size
  2. + *
  3. is url encoded by base 64 encode the random value
  4. + *
+ * @return a base64 encoded string of random bytes + */ + public String getHeaderValue() { + return RandomStringUtil.getRandomBase64EncodedString(maxHttpResponseHeaderSize + 1); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/util/RandomStringUtil.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/util/RandomStringUtil.java new file mode 100644 index 0000000000..df6d10508b --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/util/RandomStringUtil.java @@ -0,0 +1,17 @@ +package smoketest.jetty.util; + +import java.util.Base64; +import java.util.Random; + +public class RandomStringUtil { + + private RandomStringUtil() { + } + + public static String getRandomBase64EncodedString(int length) { + byte[] responseHeader = new byte[length]; + new Random().nextBytes(responseHeader); + return Base64.getEncoder().encodeToString(responseHeader); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/web/SampleController.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/web/SampleController.java index ac3973ae77..1a8c3b7ce2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/web/SampleController.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/java/smoketest/jetty/web/SampleController.java @@ -16,7 +16,9 @@ package smoketest.jetty.web; +import jakarta.servlet.http.HttpServletResponse; import smoketest.jetty.service.HelloWorldService; +import smoketest.jetty.service.HttpHeaderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -29,10 +31,21 @@ public class SampleController { @Autowired private HelloWorldService helloWorldService; + @Autowired + private HttpHeaderService httpHeaderService; + @GetMapping("/") @ResponseBody public String helloWorld() { return this.helloWorldService.getHelloMessage(); } + @GetMapping("/max-http-response-header") + @ResponseBody + public String maxHttpResponseHeader(HttpServletResponse response) { + String headerValue = httpHeaderService.getHeaderValue(); + response.addHeader("x-max-header", headerValue); + return this.helloWorldService.getHelloMessage(); + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties index eab83fbdfd..8770986768 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties @@ -1,3 +1,5 @@ server.compression.enabled: true server.compression.min-response-size: 1 +server.max-http-request-header-size=1000 server.jetty.threads.acceptors=2 +server.jetty.max-http-response-header-size=1000 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java index 049beca8bc..0803f8fb3a 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java @@ -22,9 +22,13 @@ import java.util.zip.GZIPInputStream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -32,6 +36,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.StreamUtils; +import smoketest.jetty.util.RandomStringUtil; import static org.assertj.core.api.Assertions.assertThat; @@ -40,13 +45,19 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Dave Syer * @author Andy Wilkinson + * @author Florian Storz + * @author Michael Weidmann */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ExtendWith(OutputCaptureExtension.class) class SampleJettyApplicationTests { @Autowired private TestRestTemplate restTemplate; + @Value("${server.max-http-request-header-size}") + private int maxHttpRequestHeaderSize; + @Test void testHome() { ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); @@ -66,4 +77,22 @@ class SampleJettyApplicationTests { } } + @Test + void testMaxHttpResponseHeaderSize(CapturedOutput output) { + ResponseEntity entity = this.restTemplate.getForEntity("/max-http-response-header", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(output).contains( + "org.eclipse.jetty.server.HttpChannel : handleException /max-http-response-header org.eclipse.jetty.http.BadMessageException: 500: Response header too large"); + } + + @Test + void testMaxHttpRequestHeaderSize() { + String headerValue = RandomStringUtil.getRandomBase64EncodedString(maxHttpRequestHeaderSize + 1); + HttpHeaders headers = new HttpHeaders(); + headers.add("x-max-request-header", headerValue); + HttpEntity httpEntity = new HttpEntity<>(headers); + ResponseEntity entity = this.restTemplate.exchange("/", HttpMethod.GET, httpEntity, String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE); + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/service/HttpHeaderService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/service/HttpHeaderService.java new file mode 100644 index 0000000000..be6d1a34b6 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/service/HttpHeaderService.java @@ -0,0 +1,26 @@ +package smoketest.tomcat.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import smoketest.tomcat.util.RandomStringUtil; + +@Component +public class HttpHeaderService { + + @Value("${server.tomcat.max-http-response-header-size}") + private int maxHttpResponseHeaderSize; + + /** + * generate a random byte array that + *
    + *
  1. is longer than configured + * server.jetty.max-http-response-header-size
  2. + *
  3. is url encoded by base 64 encode the random value
  4. + *
+ * @return a base64 encoded string of random bytes + */ + public String getHeaderValue() { + return RandomStringUtil.getRandomBase64EncodedString(maxHttpResponseHeaderSize + 1); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/util/RandomStringUtil.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/util/RandomStringUtil.java new file mode 100644 index 0000000000..b5f12abca8 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/util/RandomStringUtil.java @@ -0,0 +1,17 @@ +package smoketest.tomcat.util; + +import java.util.Base64; +import java.util.Random; + +public class RandomStringUtil { + + private RandomStringUtil() { + } + + public static String getRandomBase64EncodedString(int length) { + byte[] responseHeader = new byte[length]; + new Random().nextBytes(responseHeader); + return Base64.getEncoder().encodeToString(responseHeader); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/web/SampleController.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/web/SampleController.java index d5c1443417..b5aa2f79fe 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/web/SampleController.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/java/smoketest/tomcat/web/SampleController.java @@ -16,12 +16,14 @@ package smoketest.tomcat.web; +import jakarta.servlet.http.HttpServletResponse; import smoketest.tomcat.service.HelloWorldService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; +import smoketest.tomcat.service.HttpHeaderService; @Controller public class SampleController { @@ -29,10 +31,21 @@ public class SampleController { @Autowired private HelloWorldService helloWorldService; + @Autowired + private HttpHeaderService httpHeaderService; + @GetMapping("/") @ResponseBody public String helloWorld() { return this.helloWorldService.getHelloMessage(); } + @GetMapping("/max-http-response-header") + @ResponseBody + public String maxHttpResponseHeader(HttpServletResponse response) { + String headerValue = httpHeaderService.getHeaderValue(); + response.addHeader("x-max-header", headerValue); + return this.helloWorldService.getHelloMessage(); + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/resources/application.properties index 8136d016d3..2a19059833 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/main/resources/application.properties @@ -1,3 +1,5 @@ server.compression.enabled: true server.compression.min-response-size: 1 +server.max-http-request-header-size=1000 server.tomcat.connection-timeout=5s +server.tomcat.max-http-response-header-size=1000 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java index 2b1ebead6a..c8659e2cff 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java @@ -24,9 +24,13 @@ import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; @@ -37,6 +41,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.StreamUtils; +import smoketest.tomcat.util.RandomStringUtil; import static org.assertj.core.api.Assertions.assertThat; @@ -45,8 +50,11 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Dave Syer * @author Andy Wilkinson + * @author Florian Storz + * @author Michael Weidmann */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ExtendWith(OutputCaptureExtension.class) class SampleTomcatApplicationTests { @Autowired @@ -55,6 +63,9 @@ class SampleTomcatApplicationTests { @Autowired private ApplicationContext applicationContext; + @Value("${server.max-http-request-header-size}") + private int maxHttpRequestHeaderSize; + @Test void testHome() { ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); @@ -83,4 +94,23 @@ class SampleTomcatApplicationTests { assertThat(timeout).isEqualTo(5000); } + @Test + void testMaxHttpResponseHeaderSize(CapturedOutput output) { + ResponseEntity entity = this.restTemplate.getForEntity("/max-http-response-header", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(output).contains( + "threw exception [Request processing failed: org.apache.coyote.http11.HeadersTooLargeException: An attempt was made to write more data to the response headers than there was room available in the buffer. Increase maxHttpHeaderSize on the connector or write less data into the response headers.]"); + } + + @Test + void testMaxHttpRequestHeaderSize(CapturedOutput output) { + String headerValue = RandomStringUtil.getRandomBase64EncodedString(maxHttpRequestHeaderSize + 1); + HttpHeaders headers = new HttpHeaders(); + headers.add("x-max-request-header", headerValue); + HttpEntity httpEntity = new HttpEntity<>(headers); + ResponseEntity entity = this.restTemplate.exchange("/", HttpMethod.GET, httpEntity, String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(output).contains("java.lang.IllegalArgumentException: Request header is too large"); + } + }