Merge pull request #16278 from Rafiullah Hamedy

* gh-16278:
  Polish "Add support for configuring remaining Undertow server options"
  Add support for configuring remaining Undertow server options

Closes gh-16278
pull/16603/head
Andy Wilkinson 6 years ago
commit 47d42cbc35

@ -29,6 +29,8 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
import io.undertow.UndertowOptions;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ -58,6 +60,8 @@ import org.springframework.util.unit.DataSize;
* @author Chentao Qu * @author Chentao Qu
* @author Artsiom Yudovin * @author Artsiom Yudovin
* @author Andrew McGhie * @author Andrew McGhie
* @author Rafiullah Hamedy
*
*/ */
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties { public class ServerProperties {
@ -1114,6 +1118,49 @@ public class ServerProperties {
*/ */
private boolean eagerFilterInit = true; private boolean eagerFilterInit = true;
/**
* Maximum number of query or path parameters that are allowed. This limit exists
* to prevent hash collision based DOS attacks.
*/
private int maxParameters = UndertowOptions.DEFAULT_MAX_PARAMETERS;
/**
* Maximum number of headers that are allowed. This limit exists to prevent hash
* collision based DOS attacks.
*/
private int maxHeaders = UndertowOptions.DEFAULT_MAX_HEADERS;
/**
* Maximum number of cookies that are allowed. This limit exists to prevent hash
* collision based DOS attacks.
*/
private int maxCookies = 200;
/**
* Whether the server should decode percent encoded slash characters. Enabling
* encoded slashes can have security implications due to different servers
* interpreting the slash differently. Only enable this if you have a legacy
* application that requires it.
*/
private boolean allowEncodedSlash = false;
/**
* Whether the URL should be decoded. When disabled, percent-encoded characters in
* the URL will be left as-is.
*/
private boolean decodeUrl = true;
/**
* Charset used to decode URLs.
*/
private Charset urlCharset = StandardCharsets.UTF_8;
/**
* Whether the 'Connection: keep-alive' header should be added to all responses,
* even if not required by the HTTP specification.
*/
private boolean alwaysSetKeepAlive = true;
private final Accesslog accesslog = new Accesslog(); private final Accesslog accesslog = new Accesslog();
public DataSize getMaxHttpPostSize() { public DataSize getMaxHttpPostSize() {
@ -1164,6 +1211,62 @@ public class ServerProperties {
this.eagerFilterInit = eagerFilterInit; this.eagerFilterInit = eagerFilterInit;
} }
public int getMaxParameters() {
return this.maxParameters;
}
public void setMaxParameters(Integer maxParameters) {
this.maxParameters = maxParameters;
}
public int getMaxHeaders() {
return this.maxHeaders;
}
public void setMaxHeaders(int maxHeaders) {
this.maxHeaders = maxHeaders;
}
public Integer getMaxCookies() {
return this.maxCookies;
}
public void setMaxCookies(Integer maxCookies) {
this.maxCookies = maxCookies;
}
public boolean isAllowEncodedSlash() {
return this.allowEncodedSlash;
}
public void setAllowEncodedSlash(boolean allowEncodedSlash) {
this.allowEncodedSlash = allowEncodedSlash;
}
public boolean isDecodeUrl() {
return this.decodeUrl;
}
public void setDecodeUrl(Boolean decodeUrl) {
this.decodeUrl = decodeUrl;
}
public Charset getUrlCharset() {
return this.urlCharset;
}
public void setUrlCharset(Charset urlCharset) {
this.urlCharset = urlCharset;
}
public boolean isAlwaysSetKeepAlive() {
return this.alwaysSetKeepAlive;
}
public void setAlwaysSetKeepAlive(boolean alwaysSetKeepAlive) {
this.alwaysSetKeepAlive = alwaysSetKeepAlive;
}
public Accesslog getAccesslog() { public Accesslog getAccesslog() {
return this.accesslog; return this.accesslog;
} }

@ -16,9 +16,8 @@
package org.springframework.boot.autoconfigure.web.embedded; package org.springframework.boot.autoconfigure.web.embedded;
import java.time.Duration;
import io.undertow.UndertowOptions; import io.undertow.UndertowOptions;
import org.xnio.Option;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.cloud.CloudPlatform;
@ -38,6 +37,7 @@ import org.springframework.util.unit.DataSize;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Phillip Webb * @author Phillip Webb
* @author Arstiom Yudovin * @author Arstiom Yudovin
* @author Rafiullah Hamedy
* @since 2.0.0 * @since 2.0.0
*/ */
public class UndertowWebServerFactoryCustomizer implements public class UndertowWebServerFactoryCustomizer implements
@ -86,17 +86,50 @@ public class UndertowWebServerFactoryCustomizer implements
.to(factory::setAccessLogRotate); .to(factory::setAccessLogRotate);
propertyMapper.from(this::getOrDeduceUseForwardHeaders) propertyMapper.from(this::getOrDeduceUseForwardHeaders)
.to(factory::setUseForwardHeaders); .to(factory::setUseForwardHeaders);
propertyMapper.from(properties::getMaxHttpHeaderSize).whenNonNull() propertyMapper.from(properties::getMaxHttpHeaderSize).whenNonNull()
.asInt(DataSize::toBytes).when(this::isPositive) .asInt(DataSize::toBytes).when(this::isPositive)
.to((maxHttpHeaderSize) -> customizeMaxHttpHeaderSize(factory, .to((maxHttpHeaderSize) -> customizeServerOption(factory,
maxHttpHeaderSize)); UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize));
propertyMapper.from(undertowProperties::getMaxHttpPostSize)
.asInt(DataSize::toBytes).when(this::isPositive) propertyMapper.from(undertowProperties::getMaxHttpPostSize).as(DataSize::toBytes)
.to((maxHttpPostSize) -> customizeMaxHttpPostSize(factory, .when(this::isPositive)
maxHttpPostSize)); .to((maxHttpPostSize) -> customizeServerOption(factory,
UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize));
propertyMapper.from(properties::getConnectionTimeout) propertyMapper.from(properties::getConnectionTimeout)
.to((connectionTimeout) -> customizeConnectionTimeout(factory, .to((connectionTimeout) -> customizeServerOption(factory,
connectionTimeout)); UndertowOptions.NO_REQUEST_TIMEOUT,
(int) connectionTimeout.toMillis()));
propertyMapper.from(undertowProperties::getMaxParameters)
.to((maxParameters) -> customizeServerOption(factory,
UndertowOptions.MAX_PARAMETERS, maxParameters));
propertyMapper.from(undertowProperties::getMaxHeaders)
.to((maxHeaders) -> customizeServerOption(factory,
UndertowOptions.MAX_HEADERS, maxHeaders));
propertyMapper.from(undertowProperties::getMaxCookies)
.to((maxCookies) -> customizeServerOption(factory,
UndertowOptions.MAX_COOKIES, maxCookies));
propertyMapper.from(undertowProperties::isAllowEncodedSlash)
.to((allowEncodedSlash) -> customizeServerOption(factory,
UndertowOptions.ALLOW_ENCODED_SLASH, allowEncodedSlash));
propertyMapper.from(undertowProperties::isDecodeUrl)
.to((isDecodeUrl) -> customizeServerOption(factory,
UndertowOptions.DECODE_URL, isDecodeUrl));
propertyMapper.from(undertowProperties::getUrlCharset)
.to((urlCharset) -> customizeServerOption(factory,
UndertowOptions.URL_CHARSET, urlCharset.name()));
propertyMapper.from(undertowProperties::isAlwaysSetKeepAlive)
.to((alwaysSetKeepAlive) -> customizeServerOption(factory,
UndertowOptions.ALWAYS_SET_KEEP_ALIVE, alwaysSetKeepAlive));
factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo
.setEagerFilterInit(undertowProperties.isEagerFilterInit())); .setEagerFilterInit(undertowProperties.isEagerFilterInit()));
} }
@ -105,22 +138,10 @@ public class UndertowWebServerFactoryCustomizer implements
return value.longValue() > 0; return value.longValue() > 0;
} }
private void customizeConnectionTimeout(ConfigurableUndertowWebServerFactory factory, private <T> void customizeServerOption(ConfigurableUndertowWebServerFactory factory,
Duration connectionTimeout) { Option<T> option, T value) {
factory.addBuilderCustomizers((builder) -> builder.setServerOption( factory.addBuilderCustomizers(
UndertowOptions.NO_REQUEST_TIMEOUT, (int) connectionTimeout.toMillis())); (builder) -> builder.setServerOption(option, value));
}
private void customizeMaxHttpHeaderSize(ConfigurableUndertowWebServerFactory factory,
int maxHttpHeaderSize) {
factory.addBuilderCustomizers((builder) -> builder
.setServerOption(UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize));
}
private void customizeMaxHttpPostSize(ConfigurableUndertowWebServerFactory factory,
long maxHttpPostSize) {
factory.addBuilderCustomizers((builder) -> builder
.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize));
} }
private boolean getOrDeduceUseForwardHeaders() { private boolean getOrDeduceUseForwardHeaders() {

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.web.embedded; package org.springframework.boot.autoconfigure.web.embedded;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import io.undertow.Undertow; import io.undertow.Undertow;
@ -24,6 +25,7 @@ import io.undertow.Undertow.Builder;
import io.undertow.UndertowOptions; import io.undertow.UndertowOptions;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.xnio.Option;
import org.xnio.OptionMap; import org.xnio.OptionMap;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
@ -48,6 +50,8 @@ import static org.mockito.Mockito.verify;
* @author Brian Clozel * @author Brian Clozel
* @author Phillip Webb * @author Phillip Webb
* @author Artsiom Yudovin * @author Artsiom Yudovin
* @author Rafiullah Hamedy
*
*/ */
public class UndertowWebServerFactoryCustomizerTests { public class UndertowWebServerFactoryCustomizerTests {
@ -85,6 +89,78 @@ public class UndertowWebServerFactoryCustomizerTests {
verify(factory).setAccessLogRotate(false); verify(factory).setAccessLogRotate(false);
} }
@Test
public void customMaxHttpHeaderSize() {
bind("server.max-http-header-size=2048");
assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isEqualTo(2048);
}
@Test
public void customMaxHttpHeaderSizeIgnoredIfNegative() {
bind("server.max-http-header-size=-1");
assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull();
}
public void customMaxHttpHeaderSizeIgnoredIfZero() {
bind("server.max-http-header-size=0");
assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull();
}
@Test
public void customMaxHttpPostSize() {
bind("server.undertow.max-http-post-size=256");
assertThat(boundServerOption(UndertowOptions.MAX_ENTITY_SIZE)).isEqualTo(256);
}
@Test
public void customConnectionTimeout() {
bind("server.connectionTimeout=100");
assertThat(boundServerOption(UndertowOptions.NO_REQUEST_TIMEOUT)).isEqualTo(100);
}
@Test
public void customMaxParameters() {
bind("server.undertow.max-parameters=4");
assertThat(boundServerOption(UndertowOptions.MAX_PARAMETERS)).isEqualTo(4);
}
@Test
public void customMaxHeaders() {
bind("server.undertow.max-headers=4");
assertThat(boundServerOption(UndertowOptions.MAX_HEADERS)).isEqualTo(4);
}
@Test
public void customMaxCookies() {
bind("server.undertow.max-cookies=4");
assertThat(boundServerOption(UndertowOptions.MAX_COOKIES)).isEqualTo(4);
}
@Test
public void allowEncodedSlashes() {
bind("server.undertow.allow-encoded-slash=true");
assertThat(boundServerOption(UndertowOptions.ALLOW_ENCODED_SLASH)).isTrue();
}
@Test
public void disableUrlDecoding() {
bind("server.undertow.decode-url=false");
assertThat(boundServerOption(UndertowOptions.DECODE_URL)).isFalse();
}
@Test
public void customUrlCharset() {
bind("server.undertow.url-charset=UTF-16");
assertThat(boundServerOption(UndertowOptions.URL_CHARSET))
.isEqualTo(StandardCharsets.UTF_16.name());
}
@Test
public void disableAlwaysSetKeepAlive() {
bind("server.undertow.always-set-keep-alive=false");
assertThat(boundServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)).isFalse();
}
@Test @Test
public void deduceUseForwardHeaders() { public void deduceUseForwardHeaders() {
this.environment.setProperty("DYNO", "-"); this.environment.setProperty("DYNO", "-");
@ -111,49 +187,13 @@ public class UndertowWebServerFactoryCustomizerTests {
verify(factory).setUseForwardHeaders(true); verify(factory).setUseForwardHeaders(true);
} }
@Test private <T> T boundServerOption(Option<T> option) {
public void customizeMaxHttpHeaderSize() {
bind("server.max-http-header-size=2048");
Builder builder = Undertow.builder();
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
this.customizer.customize(factory);
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
"serverOptions")).getMap();
assertThat(map.get(UndertowOptions.MAX_HEADER_SIZE).intValue()).isEqualTo(2048);
}
@Test
public void customMaxHttpHeaderSizeIgnoredIfNegative() {
bind("server.max-http-header-size=-1");
Builder builder = Undertow.builder();
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
this.customizer.customize(factory);
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
"serverOptions")).getMap();
assertThat(map.contains(UndertowOptions.MAX_HEADER_SIZE)).isFalse();
}
@Test
public void customMaxHttpHeaderSizeIgnoredIfZero() {
bind("server.max-http-header-size=0");
Builder builder = Undertow.builder();
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
this.customizer.customize(factory);
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
"serverOptions")).getMap();
assertThat(map.contains(UndertowOptions.MAX_HEADER_SIZE)).isFalse();
}
@Test
public void customConnectionTimeout() {
bind("server.connection-timeout=100");
Builder builder = Undertow.builder(); Builder builder = Undertow.builder();
ConfigurableUndertowWebServerFactory factory = mockFactory(builder); ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
this.customizer.customize(factory); this.customizer.customize(factory);
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder, OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
"serverOptions")).getMap(); "serverOptions")).getMap();
assertThat(map.contains(UndertowOptions.NO_REQUEST_TIMEOUT)).isTrue(); return map.get(option);
assertThat(map.get(UndertowOptions.NO_REQUEST_TIMEOUT)).isEqualTo(100);
} }
private ConfigurableUndertowWebServerFactory mockFactory(Builder builder) { private ConfigurableUndertowWebServerFactory mockFactory(Builder builder) {

Loading…
Cancel
Save