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 04c0d81a5a..9740226d8c 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 @@ -1396,6 +1396,31 @@ public class ServerProperties { */ private Duration connectionTimeout; + /** + * Maximum content length of an H2C upgrade request. + */ + private DataSize h2cMaxContentLength = DataSize.ofBytes(0); + + /** + * Initial buffer size for HTTP request decoding. + */ + private DataSize initialBufferSize = DataSize.ofBytes(128); + + /** + * Maximum chunk size that can be decoded for an HTTP request. + */ + private DataSize maxChunkSize = DataSize.ofKilobytes(8); + + /** + * Maximum length that can be decoded for an HTTP request's initial line. + */ + private DataSize maxInitialLineLength = DataSize.ofKilobytes(4); + + /** + * Whether to validate headers when decoding requests. + */ + private boolean validateHeaders = true; + public Duration getConnectionTimeout() { return this.connectionTimeout; } @@ -1404,6 +1429,46 @@ public class ServerProperties { this.connectionTimeout = connectionTimeout; } + public DataSize getH2cMaxContentLength() { + return this.h2cMaxContentLength; + } + + public void setH2cMaxContentLength(DataSize h2cMaxContentLength) { + this.h2cMaxContentLength = h2cMaxContentLength; + } + + public DataSize getInitialBufferSize() { + return this.initialBufferSize; + } + + public void setInitialBufferSize(DataSize initialBufferSize) { + this.initialBufferSize = initialBufferSize; + } + + public DataSize getMaxChunkSize() { + return this.maxChunkSize; + } + + public void setMaxChunkSize(DataSize maxChunkSize) { + this.maxChunkSize = maxChunkSize; + } + + public DataSize getMaxInitialLineLength() { + return this.maxInitialLineLength; + } + + public void setMaxInitialLineLength(DataSize maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + } + + public boolean isValidateHeaders() { + return this.validateHeaders; + } + + public void setValidateHeaders(boolean validateHeaders) { + this.validateHeaders = validateHeaders; + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java index f2478d8e2d..2e807bd6f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java @@ -27,7 +27,6 @@ import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; -import org.springframework.util.unit.DataSize; /** * Customization for Netty-specific features. @@ -58,11 +57,10 @@ public class NettyWebServerFactoryCustomizer public void customize(NettyReactiveWebServerFactory factory) { factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); - propertyMapper.from(this.serverProperties::getMaxHttpHeaderSize) - .to((maxHttpRequestHeaderSize) -> customizeMaxHttpHeaderSize(factory, maxHttpRequestHeaderSize)); ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); propertyMapper.from(nettyProperties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); + customizeRequestDecoder(factory, propertyMapper); } private boolean getOrDeduceUseForwardHeaders() { @@ -73,14 +71,31 @@ public class NettyWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } - private void customizeMaxHttpHeaderSize(NettyReactiveWebServerFactory factory, DataSize maxHttpHeaderSize) { - factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder( - (httpRequestDecoderSpec) -> httpRequestDecoderSpec.maxHeaderSize((int) maxHttpHeaderSize.toBytes()))); - } - private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectionTimeout.toMillis())); } + private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, PropertyMapper propertyMapper) { + factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder((httpRequestDecoderSpec) -> { + propertyMapper.from(this.serverProperties.getMaxHttpHeaderSize()).whenNonNull() + .to((maxHttpRequestHeader) -> httpRequestDecoderSpec + .maxHeaderSize((int) maxHttpRequestHeader.toBytes())); + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + propertyMapper.from(nettyProperties.getMaxChunkSize()).whenNonNull() + .to((maxChunkSize) -> httpRequestDecoderSpec.maxChunkSize((int) maxChunkSize.toBytes())); + propertyMapper.from(nettyProperties.getMaxInitialLineLength()).whenNonNull() + .to((maxInitialLineLength) -> httpRequestDecoderSpec + .maxInitialLineLength((int) maxInitialLineLength.toBytes())); + propertyMapper.from(nettyProperties.getH2cMaxContentLength()).whenNonNull() + .to((h2cMaxContentLength) -> httpRequestDecoderSpec + .h2cMaxContentLength((int) h2cMaxContentLength.toBytes())); + propertyMapper.from(nettyProperties.getInitialBufferSize()).whenNonNull().to( + (initialBufferSize) -> httpRequestDecoderSpec.initialBufferSize((int) initialBufferSize.toBytes())); + propertyMapper.from(nettyProperties.isValidateHeaders()).whenNonNull() + .to(httpRequestDecoderSpec::validateHeaders); + return httpRequestDecoderSpec; + })); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index c479305808..cbc47a09e5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -43,6 +43,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.Test; +import reactor.netty.http.HttpDecoderSpec; +import reactor.netty.http.server.HttpRequestDecoderSpec; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog; import org.springframework.boot.context.properties.bind.Bindable; @@ -533,6 +535,35 @@ class ServerPropertiesTests { .isEqualTo(UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); } + @Test + void nettyMaxChunkSizeMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getMaxChunkSize().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_MAX_CHUNK_SIZE); + } + + @Test + void nettyMaxInitialLineLenghtMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getMaxInitialLineLength().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_MAX_INITIAL_LINE_LENGTH); + } + + @Test + void nettyValidateHeadersMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().isValidateHeaders()).isEqualTo(HttpDecoderSpec.DEFAULT_VALIDATE_HEADERS); + } + + @Test + void nettyH2cMaxContentLengthMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getH2cMaxContentLength().toBytes()) + .isEqualTo(HttpRequestDecoderSpec.DEFAULT_H2C_MAX_CONTENT_LENGTH); + } + + @Test + void nettyInitialBufferSizeMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getInitialBufferSize().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_INITIAL_BUFFER_SIZE); + } + private Connector getDefaultConnector() throws Exception { return new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java index 4fb11185ed..884861cb7b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.MockitoAnnotations; +import reactor.netty.http.server.HttpRequestDecoderSpec; import reactor.netty.http.server.HttpServer; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -33,6 +34,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -107,13 +109,34 @@ class NettyWebServerFactoryCustomizerTests { verifyConnectionTimeout(factory, 1000); } + @Test + void configureHttpRequestDecoder() { + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + nettyProperties.setValidateHeaders(false); + nettyProperties.setInitialBufferSize(DataSize.ofBytes(512)); + nettyProperties.setH2cMaxContentLength(DataSize.ofKilobytes(1)); + nettyProperties.setMaxChunkSize(DataSize.ofKilobytes(16)); + nettyProperties.setMaxInitialLineLength(DataSize.ofKilobytes(32)); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory, times(1)).addServerCustomizers(this.customizerCaptor.capture()); + NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); + HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); + HttpRequestDecoderSpec decoder = httpServer.configuration().decoder(); + assertThat(decoder.validateHeaders()).isFalse(); + assertThat(decoder.initialBufferSize()).isEqualTo(nettyProperties.getInitialBufferSize().toBytes()); + assertThat(decoder.h2cMaxContentLength()).isEqualTo(nettyProperties.getH2cMaxContentLength().toBytes()); + assertThat(decoder.maxChunkSize()).isEqualTo(nettyProperties.getMaxChunkSize().toBytes()); + assertThat(decoder.maxInitialLineLength()).isEqualTo(nettyProperties.getMaxInitialLineLength().toBytes()); + } + private void verifyConnectionTimeout(NettyReactiveWebServerFactory factory, Integer expected) { if (expected == null) { verify(factory, never()).addServerCustomizers(any(NettyServerCustomizer.class)); return; } - verify(factory, times(1)).addServerCustomizers(this.customizerCaptor.capture()); - NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); + verify(factory, times(2)).addServerCustomizers(this.customizerCaptor.capture()); + NettyServerCustomizer serverCustomizer = this.customizerCaptor.getAllValues().get(0); HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); Map, ?> options = httpServer.configuration().options(); assertThat(options.get(ChannelOption.CONNECT_TIMEOUT_MILLIS)).isEqualTo(expected);