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