diff --git a/src/main/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfiguration.java b/src/main/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfiguration.java new file mode 100644 index 0000000000..1e1689f627 --- /dev/null +++ b/src/main/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfiguration.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.ipc.netty.http.server; + +import java.util.function.Function; + +import io.netty.util.AttributeKey; +import reactor.ipc.netty.tcp.TcpServer; + +/** + * A configuration builder to fine tune the {@link io.netty.handler.codec.http.HttpServerCodec} + * (or more precisely the {@link io.netty.handler.codec.http.HttpServerCodec.HttpServerRequestDecoder}). + *

+ * Defaults are accessible as constants {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}, {@link #DEFAULT_MAX_HEADER_SIZE} + * and {@link #DEFAULT_MAX_CHUNK_SIZE}. + * + * @author Simon Baslé + */ +public final class HttpRequestDecoderConfiguration { + + public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096; + public static final int DEFAULT_MAX_HEADER_SIZE = 8192; + public static final int DEFAULT_MAX_CHUNK_SIZE = 8192; + public static final boolean DEFAULT_VALIDATE_HEADERS = true; + public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; + + static final AttributeKey MAX_INITIAL_LINE_LENGTH = AttributeKey.newInstance("httpCodecMaxInitialLineLength"); + static final AttributeKey MAX_HEADER_SIZE = AttributeKey.newInstance("httpCodecMaxHeaderSize"); + static final AttributeKey MAX_CHUNK_SIZE = AttributeKey.newInstance("httpCodecMaxChunkSize"); + static final AttributeKey VALIDATE_HEADERS = AttributeKey.newInstance("httpCodecValidateHeaders"); + static final AttributeKey INITIAL_BUFFER_SIZE = AttributeKey.newInstance("httpCodecInitialBufferSize"); + + int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH; + int maxHeaderSize = DEFAULT_MAX_HEADER_SIZE; + int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE; + boolean validateHeaders = DEFAULT_VALIDATE_HEADERS; + int initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE; + + /** + * Configure the maximum length that can be decoded for the HTTP request's initial + * line. Defaults to {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}. + * + * @param value the value for the maximum initial line length (strictly positive) + * @return this option builder for further configuration + */ + public HttpRequestDecoderConfiguration maxInitialLineLength(int value) { + if (value <= 0) { + throw new IllegalArgumentException( + "maxInitialLineLength must be strictly positive"); + } + this.maxInitialLineLength = value; + return this; + } + + /** + * Configure the maximum header size that can be decoded for the HTTP request. + * Defaults to {@link #DEFAULT_MAX_HEADER_SIZE}. + * + * @param value the value for the maximum header size (strictly positive) + * @return this option builder for further configuration + */ + public HttpRequestDecoderConfiguration maxHeaderSize(int value) { + if (value <= 0) { + throw new IllegalArgumentException("maxHeaderSize must be strictly positive"); + } + this.maxHeaderSize = value; + return this; + } + + /** + * Configure the maximum chunk size that can be decoded for the HTTP request. + * Defaults to {@link #DEFAULT_MAX_CHUNK_SIZE}. + * + * @param value the value for the maximum chunk size (strictly positive) + * @return this option builder for further configuration + */ + public HttpRequestDecoderConfiguration maxChunkSize(int value) { + if (value <= 0) { + throw new IllegalArgumentException("maxChunkSize must be strictly positive"); + } + this.maxChunkSize = value; + return this; + } + + /** + * Configure whether or not to validate headers when decoding requests. Defaults to + * #DEFAULT_VALIDATE_HEADERS. + * + * @param validate true to validate headers, false otherwise + * @return this option builder for further configuration + */ + public HttpRequestDecoderConfiguration validateHeaders(boolean validate) { + this.validateHeaders = validate; + return this; + } + + /** + * Configure the initial buffer size for HTTP request decoding. Defaults to + * {@link #DEFAULT_INITIAL_BUFFER_SIZE}. + * + * @param value the initial buffer size to use (strictly positive) + * @return + */ + public HttpRequestDecoderConfiguration initialBufferSize(int value) { + if (value <= 0) { + throw new IllegalArgumentException("initialBufferSize must be strictly positive"); + } + this.initialBufferSize = value; + return this; + } + + /** + * Build a {@link Function} that applies the http request decoder configuration to a + * {@link TcpServer} by enriching its attributes. + */ + Function build() { + return tcp -> tcp.selectorAttr(MAX_INITIAL_LINE_LENGTH, maxInitialLineLength) + .selectorAttr(MAX_HEADER_SIZE, maxHeaderSize) + .selectorAttr(MAX_CHUNK_SIZE, maxChunkSize) + .selectorAttr(VALIDATE_HEADERS, validateHeaders) + .selectorAttr(INITIAL_BUFFER_SIZE, initialBufferSize); + } + +} diff --git a/src/main/java/reactor/ipc/netty/http/server/HttpServer.java b/src/main/java/reactor/ipc/netty/http/server/HttpServer.java index b887440ae5..73ba3a5133 100644 --- a/src/main/java/reactor/ipc/netty/http/server/HttpServer.java +++ b/src/main/java/reactor/ipc/netty/http/server/HttpServer.java @@ -235,6 +235,18 @@ public final HttpServer noCompression() { return tcpConfiguration(COMPRESS_ATTR_DISABLE); } + /** + * Configure the {@link io.netty.handler.codec.http.HttpServerCodec}'s request decoding options. + * + * @param requestDecoderOptions a function to mutate the provided Http request decoder options + * @return a new {@link HttpServer} + */ + public final HttpServer httpRequestDecoder(Function requestDecoderOptions) { + return tcpConfiguration( + requestDecoderOptions.apply(new HttpRequestDecoderConfiguration()) + .build()); + } + /** * Disable support for the {@code "Forwarded"} and {@code "X-Forwarded-*"} * HTTP request headers. diff --git a/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java b/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java index 18c13939c3..32161626cd 100644 --- a/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java +++ b/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java @@ -33,6 +33,17 @@ import reactor.ipc.netty.resources.LoopResources; import reactor.ipc.netty.tcp.TcpServer; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.INITIAL_BUFFER_SIZE; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.MAX_CHUNK_SIZE; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.MAX_HEADER_SIZE; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.MAX_INITIAL_LINE_LENGTH; +import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.VALIDATE_HEADERS; + /** * @author Stephane Maldini */ @@ -81,18 +92,24 @@ public Mono bind(TcpServer delegate) { .channel(loops.onServerChannel(elg)); } - Integer minCompressionSize = (Integer) b.config() - .attrs() - .get(PRODUCE_GZIP); + Integer minCompressionSize = getAttributeValue(b, PRODUCE_GZIP, null); + + Integer line = getAttributeValue(b, MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_INITIAL_LINE_LENGTH); + + Integer header = getAttributeValue(b, MAX_HEADER_SIZE, DEFAULT_MAX_HEADER_SIZE); + + Integer chunk = getAttributeValue(b, MAX_CHUNK_SIZE, DEFAULT_MAX_CHUNK_SIZE); - b.attr(PRODUCE_GZIP, null); + Boolean validate = getAttributeValue(b, VALIDATE_HEADERS, DEFAULT_VALIDATE_HEADERS); + + Integer buffer = getAttributeValue(b, INITIAL_BUFFER_SIZE, DEFAULT_INITIAL_BUFFER_SIZE); BootstrapHandlers.updateConfiguration(b, NettyPipeline.HttpInitializer, (listener, channel) -> { ChannelPipeline p = channel.pipeline(); - p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec()); + p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec(line, header, chunk, validate, buffer)); if (minCompressionSize != null && minCompressionSize >= 0) { p.addLast(NettyPipeline.CompressionHandler, @@ -108,4 +125,12 @@ public Mono bind(TcpServer delegate) { static final AttributeKey PRODUCE_GZIP = AttributeKey.newInstance("produceGzip"); + + private T getAttributeValue(ServerBootstrap bootstrap, AttributeKey attributeKey, T defaultValue) { + T result = bootstrap.config().attrs().get(attributeKey) != null + ? (T) bootstrap.config().attrs().get(attributeKey) + : defaultValue; + bootstrap.attr(attributeKey, null); + return result; + } } diff --git a/src/test/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfigurationTest.java b/src/test/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfigurationTest.java new file mode 100644 index 0000000000..a9e8f6ae6e --- /dev/null +++ b/src/test/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfigurationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.ipc.netty.http.server; + +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class HttpRequestDecoderConfigurationTest { + + private HttpRequestDecoderConfiguration conf; + + @Before + public void init() { + conf = new HttpRequestDecoderConfiguration(); + } + + @Test + public void maxInitialLineLength() { + conf.maxInitialLineLength(123); + + assertThat(conf.maxInitialLineLength).as("initial line length").isEqualTo(123); + + assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE); + assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE); + assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS); + assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE); + } + + @Test + public void maxInitialLineLengthBadValues() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.maxInitialLineLength(0)) + .as("rejects 0") + .withMessage("maxInitialLineLength must be strictly positive"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.maxInitialLineLength(-1)) + .as("rejects negative") + .withMessage("maxInitialLineLength must be strictly positive"); + } + + @Test + public void maxHeaderSize() { + conf.maxHeaderSize(123); + + assertThat(conf.maxHeaderSize).as("header size").isEqualTo(123); + + assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH); + assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE); + assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS); + assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE); + } + + @Test + public void maxHeaderSizeBadValues() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.maxHeaderSize(0)) + .as("rejects 0") + .withMessage("maxHeaderSize must be strictly positive"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.maxHeaderSize(-1)) + .as("rejects negative") + .withMessage("maxHeaderSize must be strictly positive"); + } + + @Test + public void maxChunkSize() { + conf.maxChunkSize(123); + + assertThat(conf.maxChunkSize).as("chunk size").isEqualTo(123); + + assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH); + assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE); + assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS); + assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE); + } + + @Test + public void maxChunkSizeBadValues() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.maxChunkSize(0)) + .as("rejects 0") + .withMessage("maxChunkSize must be strictly positive"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.maxChunkSize(-1)) + .as("rejects negative") + .withMessage("maxChunkSize must be strictly positive"); + } + + @Test + public void validateHeaders() { + conf.validateHeaders(false); + + assertThat(conf.validateHeaders).as("validate headers").isFalse(); + + assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH); + assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE); + assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE); + assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE); + } + + @Test + public void initialBufferSize() { + conf.initialBufferSize(123); + + assertThat(conf.initialBufferSize).as("initial buffer size").isEqualTo(123); + + assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH); + assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE); + assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE); + assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS); + } + + @Test + public void initialBufferSizeBadValues() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.initialBufferSize(0)) + .as("rejects 0") + .withMessage("initialBufferSize must be strictly positive"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> conf.initialBufferSize(-1)) + .as("rejects negative") + .withMessage("initialBufferSize must be strictly positive"); + } +} \ No newline at end of file diff --git a/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java b/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java index a894abbd59..e0cb72f008 100644 --- a/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java +++ b/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java @@ -17,6 +17,7 @@ package reactor.ipc.netty.http.server; import java.io.IOException; +import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.net.URISyntaxException; import java.nio.ByteBuffer; @@ -55,7 +56,9 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectDecoder; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; @@ -966,4 +969,65 @@ public void redirectTests(String url) { } } + + @Test + public void httpServerRequestConfigInjectAttributes() { + AtomicReference channelRef = new AtomicReference<>(); + HttpServer server = + HttpServer.create() + .httpRequestDecoder(opt -> opt.maxInitialLineLength(123) + .maxHeaderSize(456) + .maxChunkSize(789) + .validateHeaders(false) + .initialBufferSize(10)) + .handler((req, resp) -> resp.sendNotFound()) + .tcpConfiguration(tcp -> tcp.doOnConnection(c -> channelRef.set(c.channel()))) + .wiretap(); + + DisposableServer ds = server.bindNow(); + + HttpClient.prepare() + .addressSupplier(ds::address) + .post() + .uri("/") + .send(ByteBufFlux.fromString(Mono.just("bodysample"))) + .responseContent() + .aggregate() + .asString() + .block(); + + assertThat(channelRef.get()).isNotNull(); + Channel c = channelRef.get(); + HttpServerCodec codec = c.pipeline().get(HttpServerCodec.class); + HttpObjectDecoder decoder = (HttpObjectDecoder) getValueReflection(codec, "inboundHandler", 1); + int chunkSize = (Integer) getValueReflection(decoder, "maxChunkSize", 2); + boolean validate = (Boolean) getValueReflection(decoder, "validateHeaders", 2); + + ds.disposeNow(); + + assertThat(chunkSize).as("line length").isEqualTo(789); + assertThat(validate).as("validate headers").isFalse(); + } + + private Object getValueReflection(Object obj, String fieldName, int superLevel) { + try { + Field field; + if (superLevel == 1) { + field = obj.getClass() + .getSuperclass() + .getDeclaredField(fieldName); + } + else { + field = obj.getClass() + .getSuperclass() + .getSuperclass() + .getDeclaredField(fieldName); + } + field.setAccessible(true); + return field.get(obj); + } + catch(NoSuchFieldException | IllegalAccessException e) { + return null; + } + } }