From f068f9fc525ba16353c09ac4ddc48bb0cc1df6d5 Mon Sep 17 00:00:00 2001 From: Julien Eyraud Date: Fri, 17 Jul 2020 00:45:10 +0200 Subject: [PATCH] Add properties for Netty HttpDecoderSpec See gh-22367 --- .../autoconfigure/web/ServerProperties.java | 65 +++++++++++++++++++ .../NettyWebServerFactoryCustomizer.java | 35 ++++++++-- .../NettyWebServerFactoryCustomizerTests.java | 31 +++++++++ 3 files changed, 125 insertions(+), 6 deletions(-) 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..cd004a92c3 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; + /** + * The maximum chunk size that can be decoded for the HTTP request. + */ + private DataSize maxChunkSize; + + /** + * The maximum length that can be decoded for the HTTP request's initial line. + */ + private DataSize maxInitialLineLength; + + /** + * Configure whether or not to validate headers when decoding requests. + */ + private Boolean validateHeaders; + + /** + * The maximum length of the content of the HTTP/2.0 clear-text upgrade request. + */ + private DataSize h2cMaxContentLength; + + /** + * The initial buffer size for HTTP request decoding. + */ + private DataSize initialBufferSize; + public Duration getConnectionTimeout() { return this.connectionTimeout; } @@ -1404,6 +1429,46 @@ public class ServerProperties { this.connectionTimeout = connectionTimeout; } + 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 getValidateHeaders() { + return this.validateHeaders; + } + + public void setValidateHeaders(Boolean validateHeaders) { + this.validateHeaders = validateHeaders; + } + + 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; + } + } /** 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..4920057cdc 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 @@ -17,6 +17,8 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.time.Duration; +import java.util.Objects; +import java.util.stream.Stream; import io.netty.channel.ChannelOption; @@ -27,7 +29,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 +59,16 @@ 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)); + if (Stream + .of(this.serverProperties.getMaxHttpHeaderSize(), nettyProperties.getH2cMaxContentLength(), + nettyProperties.getMaxChunkSize(), nettyProperties.getMaxInitialLineLength(), + nettyProperties.getValidateHeaders(), nettyProperties.getInitialBufferSize()) + .anyMatch(Objects::nonNull)) { + customizeRequestDecoder(factory, propertyMapper); + } } private boolean getOrDeduceUseForwardHeaders() { @@ -73,9 +79,26 @@ 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 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.getValidateHeaders()).whenNonNull() + .to(httpRequestDecoderSpec::validateHeaders); + return httpRequestDecoderSpec; + })); } private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { 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..cba78dc385 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,6 +109,35 @@ class NettyWebServerFactoryCustomizerTests { verifyConnectionTimeout(factory, 1000); } + @Test + void setHttpRequestDecoder() { + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + nettyProperties.setValidateHeaders(true); + 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()).isTrue(); + 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()); + } + + @Test + void shouldNotSetAnyHttpRequestDecoderProperties() { + this.serverProperties.setMaxHttpHeaderSize(null); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory, never()).addServerCustomizers(this.customizerCaptor.capture()); + } + private void verifyConnectionTimeout(NettyReactiveWebServerFactory factory, Integer expected) { if (expected == null) { verify(factory, never()).addServerCustomizers(any(NettyServerCustomizer.class));