Skip to content

Commit

Permalink
fix #150 Add a configuration API for HttpCodec
Browse files Browse the repository at this point in the history
This commits allows to configure Netty's HttpServerCodec 5 options for
request decoding, through an HttpServer API that consumes a small simple
builder which holds the parameters. Parameters are then injected as
Channel attributes.

This would notably allow the codec to decode requests on very long URIs.
  • Loading branch information
simonbasle authored and violetagg committed Jan 24, 2018
1 parent cabe50f commit a3ed0a4
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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}).
* <p>
* 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<Integer> MAX_INITIAL_LINE_LENGTH = AttributeKey.newInstance("httpCodecMaxInitialLineLength");
static final AttributeKey<Integer> MAX_HEADER_SIZE = AttributeKey.newInstance("httpCodecMaxHeaderSize");
static final AttributeKey<Integer> MAX_CHUNK_SIZE = AttributeKey.newInstance("httpCodecMaxChunkSize");
static final AttributeKey<Boolean> VALIDATE_HEADERS = AttributeKey.newInstance("httpCodecValidateHeaders");
static final AttributeKey<Integer> 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<TcpServer, TcpServer> 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);
}

}
12 changes: 12 additions & 0 deletions src/main/java/reactor/ipc/netty/http/server/HttpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpRequestDecoderConfiguration, HttpRequestDecoderConfiguration> requestDecoderOptions) {
return tcpConfiguration(
requestDecoderOptions.apply(new HttpRequestDecoderConfiguration())
.build());
}

/**
* Disable support for the {@code "Forwarded"} and {@code "X-Forwarded-*"}
* HTTP request headers.
Expand Down
35 changes: 30 additions & 5 deletions src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -81,18 +92,24 @@ public Mono<? extends DisposableServer> 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,
Expand All @@ -108,4 +125,12 @@ public Mono<? extends DisposableServer> bind(TcpServer delegate) {

static final AttributeKey<Integer> PRODUCE_GZIP =
AttributeKey.newInstance("produceGzip");

private <T> T getAttributeValue(ServerBootstrap bootstrap, AttributeKey<T> attributeKey, T defaultValue) {
T result = bootstrap.config().attrs().get(attributeKey) != null
? (T) bootstrap.config().attrs().get(attributeKey)
: defaultValue;
bootstrap.attr(attributeKey, null);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading

0 comments on commit a3ed0a4

Please sign in to comment.