Add max http response header size configuration for tomcat and jetty

See gh-33553
This commit is contained in:
Michael Weidmann 2022-12-20 16:51:33 +01:00 committed by Moritz Halbritter
parent ace31cd5b2
commit 93d46d11e9
15 changed files with 406 additions and 73 deletions

View File

@ -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.
*/

View File

@ -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,13 +197,7 @@ public class JettyWebServerFactoryCustomizer
return CustomRequestLog.NCSA_FORMAT;
}
private static class MaxHttpRequestHeaderSizeCustomizer implements JettyServerCustomizer {
private final int maxRequestHeaderSize;
MaxHttpRequestHeaderSizeCustomizer(int maxRequestHeaderSize) {
this.maxRequestHeaderSize = maxRequestHeaderSize;
}
private record MaxHttpRequestHeaderSizeCustomizer(int maxRequestHeaderSize) implements JettyServerCustomizer {
@Override
public void customize(Server server) {
@ -215,7 +214,25 @@ public class JettyWebServerFactoryCustomizer
.setRequestHeaderSize(this.maxRequestHeaderSize);
}
}
}
private record MaxHttpResponseHeaderSizeCustomizer(int maxResponseHeaderSize) implements JettyServerCustomizer {
@Override
public void customize(Server server) {
Arrays.stream(server.getConnectors()).forEach(this::customize);
}
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()
.setResponseHeaderSize(this.maxResponseHeaderSize);
}
}
}
}

View File

@ -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 <T extends ProtocolHandler> void customizeHandler(ConfigurableTomcatWebServerFactory factory, int value,
Class<T> type, ObjIntConsumer<T> 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);
}
});
}

View File

@ -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<Integer> requestHeaderSizes = getRequestHeaderSizes(server);
assertThat(requestHeaderSizes).containsOnly(2048);
}
@Test
void customMaxHttpRequestHeaderSizeIgnoredIfNegative() {
bind("server.max-http-request-header-size=-1");
JettyWebServer server = customizeAndGetServer();
List<Integer> requestHeaderSizes = getRequestHeaderSizes(server);
assertThat(requestHeaderSizes).containsOnly(8192);
}
@Test
void customMaxHttpRequestHeaderSizeIgnoredIfZero() {
bind("server.max-http-request-header-size=0");
JettyWebServer server = customizeAndGetServer();
List<Integer> requestHeaderSizes = getRequestHeaderSizes(server);
assertThat(requestHeaderSizes).containsOnly(8192);
}
@Test
void defaultMaxHttpResponseHeaderSize() {
JettyWebServer server = customizeAndGetServer();
List<Integer> responseHeaderSizes = getResponseHeaderSizes(server);
assertThat(responseHeaderSizes).containsOnly(8192);
}
@Test
void customizeMaxHttpResponseHeaderSize() {
bind("server.jetty.max-http-response-header-size=2KB");
JettyWebServer server = customizeAndGetServer();
List<Integer> responseHeaderSizes = getResponseHeaderSizes(server);
assertThat(responseHeaderSizes).containsOnly(2048);
}
@Test
void customMaxHttpResponseHeaderSizeIgnoredIfNegative() {
bind("server.jetty.max-http-response-header-size=-1");
JettyWebServer server = customizeAndGetServer();
List<Integer> responseHeaderSizes = getResponseHeaderSizes(server);
assertThat(responseHeaderSizes).containsOnly(8192);
}
@Test
void customMaxHttpResponseHeaderSizeIgnoredIfZero() {
bind("server.jetty.max-http-response-header-size=0");
JettyWebServer server = customizeAndGetServer();
List<Integer> 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<Integer> getRequestHeaderSizes(JettyWebServer server) {
return getHeaderSizes(server, HttpConfiguration::getRequestHeaderSize);
}
private List<Integer> getResponseHeaderSizes(JettyWebServer server) {
return getHeaderSizes(server, HttpConfiguration::getResponseHeaderSize);
}
private List<Integer> getHeaderSizes(JettyWebServer server, Function<HttpConfiguration, Integer> provider) {
List<Integer> 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;

View File

@ -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");

View File

@ -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
* <ol>
* <li>is longer than configured
* <code>server.jetty.max-http-response-header-size</code></li>
* <li>is url encoded by base 64 encode the random value</li>
* </ol>
* @return a base64 encoded string of random bytes
*/
public String getHeaderValue() {
return RandomStringUtil.getRandomBase64EncodedString(maxHttpResponseHeaderSize + 1);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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<String> entity = this.restTemplate.getForEntity("/", String.class);
@ -66,4 +77,22 @@ class SampleJettyApplicationTests {
}
}
@Test
void testMaxHttpResponseHeaderSize(CapturedOutput output) {
ResponseEntity<String> 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<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, httpEntity, String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE);
}
}

View File

@ -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
* <ol>
* <li>is longer than configured
* <code>server.jetty.max-http-response-header-size</code></li>
* <li>is url encoded by base 64 encode the random value</li>
* </ol>
* @return a base64 encoded string of random bytes
*/
public String getHeaderValue() {
return RandomStringUtil.getRandomBase64EncodedString(maxHttpResponseHeaderSize + 1);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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<String> entity = this.restTemplate.getForEntity("/", String.class);
@ -83,4 +94,23 @@ class SampleTomcatApplicationTests {
assertThat(timeout).isEqualTo(5000);
}
@Test
void testMaxHttpResponseHeaderSize(CapturedOutput output) {
ResponseEntity<String> 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<String> 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");
}
}