Skip to content

Commit 47d42cb

Browse files
committed
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
2 parents 13b356a + ca1a666 commit 47d42cb

File tree

3 files changed

+228
-64
lines changed

3 files changed

+228
-64
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.util.Map;
3030
import java.util.TimeZone;
3131

32+
import io.undertow.UndertowOptions;
33+
3234
import org.springframework.boot.context.properties.ConfigurationProperties;
3335
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
3436
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@@ -58,6 +60,8 @@
5860
* @author Chentao Qu
5961
* @author Artsiom Yudovin
6062
* @author Andrew McGhie
63+
* @author Rafiullah Hamedy
64+
*
6165
*/
6266
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
6367
public class ServerProperties {
@@ -1114,6 +1118,49 @@ public static class Undertow {
11141118
*/
11151119
private boolean eagerFilterInit = true;
11161120

1121+
/**
1122+
* Maximum number of query or path parameters that are allowed. This limit exists
1123+
* to prevent hash collision based DOS attacks.
1124+
*/
1125+
private int maxParameters = UndertowOptions.DEFAULT_MAX_PARAMETERS;
1126+
1127+
/**
1128+
* Maximum number of headers that are allowed. This limit exists to prevent hash
1129+
* collision based DOS attacks.
1130+
*/
1131+
private int maxHeaders = UndertowOptions.DEFAULT_MAX_HEADERS;
1132+
1133+
/**
1134+
* Maximum number of cookies that are allowed. This limit exists to prevent hash
1135+
* collision based DOS attacks.
1136+
*/
1137+
private int maxCookies = 200;
1138+
1139+
/**
1140+
* Whether the server should decode percent encoded slash characters. Enabling
1141+
* encoded slashes can have security implications due to different servers
1142+
* interpreting the slash differently. Only enable this if you have a legacy
1143+
* application that requires it.
1144+
*/
1145+
private boolean allowEncodedSlash = false;
1146+
1147+
/**
1148+
* Whether the URL should be decoded. When disabled, percent-encoded characters in
1149+
* the URL will be left as-is.
1150+
*/
1151+
private boolean decodeUrl = true;
1152+
1153+
/**
1154+
* Charset used to decode URLs.
1155+
*/
1156+
private Charset urlCharset = StandardCharsets.UTF_8;
1157+
1158+
/**
1159+
* Whether the 'Connection: keep-alive' header should be added to all responses,
1160+
* even if not required by the HTTP specification.
1161+
*/
1162+
private boolean alwaysSetKeepAlive = true;
1163+
11171164
private final Accesslog accesslog = new Accesslog();
11181165

11191166
public DataSize getMaxHttpPostSize() {
@@ -1164,6 +1211,62 @@ public void setEagerFilterInit(boolean eagerFilterInit) {
11641211
this.eagerFilterInit = eagerFilterInit;
11651212
}
11661213

1214+
public int getMaxParameters() {
1215+
return this.maxParameters;
1216+
}
1217+
1218+
public void setMaxParameters(Integer maxParameters) {
1219+
this.maxParameters = maxParameters;
1220+
}
1221+
1222+
public int getMaxHeaders() {
1223+
return this.maxHeaders;
1224+
}
1225+
1226+
public void setMaxHeaders(int maxHeaders) {
1227+
this.maxHeaders = maxHeaders;
1228+
}
1229+
1230+
public Integer getMaxCookies() {
1231+
return this.maxCookies;
1232+
}
1233+
1234+
public void setMaxCookies(Integer maxCookies) {
1235+
this.maxCookies = maxCookies;
1236+
}
1237+
1238+
public boolean isAllowEncodedSlash() {
1239+
return this.allowEncodedSlash;
1240+
}
1241+
1242+
public void setAllowEncodedSlash(boolean allowEncodedSlash) {
1243+
this.allowEncodedSlash = allowEncodedSlash;
1244+
}
1245+
1246+
public boolean isDecodeUrl() {
1247+
return this.decodeUrl;
1248+
}
1249+
1250+
public void setDecodeUrl(Boolean decodeUrl) {
1251+
this.decodeUrl = decodeUrl;
1252+
}
1253+
1254+
public Charset getUrlCharset() {
1255+
return this.urlCharset;
1256+
}
1257+
1258+
public void setUrlCharset(Charset urlCharset) {
1259+
this.urlCharset = urlCharset;
1260+
}
1261+
1262+
public boolean isAlwaysSetKeepAlive() {
1263+
return this.alwaysSetKeepAlive;
1264+
}
1265+
1266+
public void setAlwaysSetKeepAlive(boolean alwaysSetKeepAlive) {
1267+
this.alwaysSetKeepAlive = alwaysSetKeepAlive;
1268+
}
1269+
11671270
public Accesslog getAccesslog() {
11681271
return this.accesslog;
11691272
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616

1717
package org.springframework.boot.autoconfigure.web.embedded;
1818

19-
import java.time.Duration;
20-
2119
import io.undertow.UndertowOptions;
20+
import org.xnio.Option;
2221

2322
import org.springframework.boot.autoconfigure.web.ServerProperties;
2423
import org.springframework.boot.cloud.CloudPlatform;
@@ -38,6 +37,7 @@
3837
* @author Stephane Nicoll
3938
* @author Phillip Webb
4039
* @author Arstiom Yudovin
40+
* @author Rafiullah Hamedy
4141
* @since 2.0.0
4242
*/
4343
public class UndertowWebServerFactoryCustomizer implements
@@ -86,17 +86,50 @@ public void customize(ConfigurableUndertowWebServerFactory factory) {
8686
.to(factory::setAccessLogRotate);
8787
propertyMapper.from(this::getOrDeduceUseForwardHeaders)
8888
.to(factory::setUseForwardHeaders);
89+
8990
propertyMapper.from(properties::getMaxHttpHeaderSize).whenNonNull()
9091
.asInt(DataSize::toBytes).when(this::isPositive)
91-
.to((maxHttpHeaderSize) -> customizeMaxHttpHeaderSize(factory,
92-
maxHttpHeaderSize));
93-
propertyMapper.from(undertowProperties::getMaxHttpPostSize)
94-
.asInt(DataSize::toBytes).when(this::isPositive)
95-
.to((maxHttpPostSize) -> customizeMaxHttpPostSize(factory,
96-
maxHttpPostSize));
92+
.to((maxHttpHeaderSize) -> customizeServerOption(factory,
93+
UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize));
94+
95+
propertyMapper.from(undertowProperties::getMaxHttpPostSize).as(DataSize::toBytes)
96+
.when(this::isPositive)
97+
.to((maxHttpPostSize) -> customizeServerOption(factory,
98+
UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize));
99+
97100
propertyMapper.from(properties::getConnectionTimeout)
98-
.to((connectionTimeout) -> customizeConnectionTimeout(factory,
99-
connectionTimeout));
101+
.to((connectionTimeout) -> customizeServerOption(factory,
102+
UndertowOptions.NO_REQUEST_TIMEOUT,
103+
(int) connectionTimeout.toMillis()));
104+
105+
propertyMapper.from(undertowProperties::getMaxParameters)
106+
.to((maxParameters) -> customizeServerOption(factory,
107+
UndertowOptions.MAX_PARAMETERS, maxParameters));
108+
109+
propertyMapper.from(undertowProperties::getMaxHeaders)
110+
.to((maxHeaders) -> customizeServerOption(factory,
111+
UndertowOptions.MAX_HEADERS, maxHeaders));
112+
113+
propertyMapper.from(undertowProperties::getMaxCookies)
114+
.to((maxCookies) -> customizeServerOption(factory,
115+
UndertowOptions.MAX_COOKIES, maxCookies));
116+
117+
propertyMapper.from(undertowProperties::isAllowEncodedSlash)
118+
.to((allowEncodedSlash) -> customizeServerOption(factory,
119+
UndertowOptions.ALLOW_ENCODED_SLASH, allowEncodedSlash));
120+
121+
propertyMapper.from(undertowProperties::isDecodeUrl)
122+
.to((isDecodeUrl) -> customizeServerOption(factory,
123+
UndertowOptions.DECODE_URL, isDecodeUrl));
124+
125+
propertyMapper.from(undertowProperties::getUrlCharset)
126+
.to((urlCharset) -> customizeServerOption(factory,
127+
UndertowOptions.URL_CHARSET, urlCharset.name()));
128+
129+
propertyMapper.from(undertowProperties::isAlwaysSetKeepAlive)
130+
.to((alwaysSetKeepAlive) -> customizeServerOption(factory,
131+
UndertowOptions.ALWAYS_SET_KEEP_ALIVE, alwaysSetKeepAlive));
132+
100133
factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo
101134
.setEagerFilterInit(undertowProperties.isEagerFilterInit()));
102135
}
@@ -105,22 +138,10 @@ private boolean isPositive(Number value) {
105138
return value.longValue() > 0;
106139
}
107140

108-
private void customizeConnectionTimeout(ConfigurableUndertowWebServerFactory factory,
109-
Duration connectionTimeout) {
110-
factory.addBuilderCustomizers((builder) -> builder.setServerOption(
111-
UndertowOptions.NO_REQUEST_TIMEOUT, (int) connectionTimeout.toMillis()));
112-
}
113-
114-
private void customizeMaxHttpHeaderSize(ConfigurableUndertowWebServerFactory factory,
115-
int maxHttpHeaderSize) {
116-
factory.addBuilderCustomizers((builder) -> builder
117-
.setServerOption(UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize));
118-
}
119-
120-
private void customizeMaxHttpPostSize(ConfigurableUndertowWebServerFactory factory,
121-
long maxHttpPostSize) {
122-
factory.addBuilderCustomizers((builder) -> builder
123-
.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize));
141+
private <T> void customizeServerOption(ConfigurableUndertowWebServerFactory factory,
142+
Option<T> option, T value) {
143+
factory.addBuilderCustomizers(
144+
(builder) -> builder.setServerOption(option, value));
124145
}
125146

126147
private boolean getOrDeduceUseForwardHeaders() {

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package org.springframework.boot.autoconfigure.web.embedded;
1818

1919
import java.io.File;
20+
import java.nio.charset.StandardCharsets;
2021
import java.util.Arrays;
2122

2223
import io.undertow.Undertow;
2324
import io.undertow.Undertow.Builder;
2425
import io.undertow.UndertowOptions;
2526
import org.junit.Before;
2627
import org.junit.Test;
28+
import org.xnio.Option;
2729
import org.xnio.OptionMap;
2830

2931
import org.springframework.boot.autoconfigure.web.ServerProperties;
@@ -48,6 +50,8 @@
4850
* @author Brian Clozel
4951
* @author Phillip Webb
5052
* @author Artsiom Yudovin
53+
* @author Rafiullah Hamedy
54+
*
5155
*/
5256
public class UndertowWebServerFactoryCustomizerTests {
5357

@@ -85,6 +89,78 @@ public void customizeUndertowAccessLog() {
8589
verify(factory).setAccessLogRotate(false);
8690
}
8791

92+
@Test
93+
public void customMaxHttpHeaderSize() {
94+
bind("server.max-http-header-size=2048");
95+
assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isEqualTo(2048);
96+
}
97+
98+
@Test
99+
public void customMaxHttpHeaderSizeIgnoredIfNegative() {
100+
bind("server.max-http-header-size=-1");
101+
assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull();
102+
}
103+
104+
public void customMaxHttpHeaderSizeIgnoredIfZero() {
105+
bind("server.max-http-header-size=0");
106+
assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull();
107+
}
108+
109+
@Test
110+
public void customMaxHttpPostSize() {
111+
bind("server.undertow.max-http-post-size=256");
112+
assertThat(boundServerOption(UndertowOptions.MAX_ENTITY_SIZE)).isEqualTo(256);
113+
}
114+
115+
@Test
116+
public void customConnectionTimeout() {
117+
bind("server.connectionTimeout=100");
118+
assertThat(boundServerOption(UndertowOptions.NO_REQUEST_TIMEOUT)).isEqualTo(100);
119+
}
120+
121+
@Test
122+
public void customMaxParameters() {
123+
bind("server.undertow.max-parameters=4");
124+
assertThat(boundServerOption(UndertowOptions.MAX_PARAMETERS)).isEqualTo(4);
125+
}
126+
127+
@Test
128+
public void customMaxHeaders() {
129+
bind("server.undertow.max-headers=4");
130+
assertThat(boundServerOption(UndertowOptions.MAX_HEADERS)).isEqualTo(4);
131+
}
132+
133+
@Test
134+
public void customMaxCookies() {
135+
bind("server.undertow.max-cookies=4");
136+
assertThat(boundServerOption(UndertowOptions.MAX_COOKIES)).isEqualTo(4);
137+
}
138+
139+
@Test
140+
public void allowEncodedSlashes() {
141+
bind("server.undertow.allow-encoded-slash=true");
142+
assertThat(boundServerOption(UndertowOptions.ALLOW_ENCODED_SLASH)).isTrue();
143+
}
144+
145+
@Test
146+
public void disableUrlDecoding() {
147+
bind("server.undertow.decode-url=false");
148+
assertThat(boundServerOption(UndertowOptions.DECODE_URL)).isFalse();
149+
}
150+
151+
@Test
152+
public void customUrlCharset() {
153+
bind("server.undertow.url-charset=UTF-16");
154+
assertThat(boundServerOption(UndertowOptions.URL_CHARSET))
155+
.isEqualTo(StandardCharsets.UTF_16.name());
156+
}
157+
158+
@Test
159+
public void disableAlwaysSetKeepAlive() {
160+
bind("server.undertow.always-set-keep-alive=false");
161+
assertThat(boundServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)).isFalse();
162+
}
163+
88164
@Test
89165
public void deduceUseForwardHeaders() {
90166
this.environment.setProperty("DYNO", "-");
@@ -111,49 +187,13 @@ public void setUseForwardHeaders() {
111187
verify(factory).setUseForwardHeaders(true);
112188
}
113189

114-
@Test
115-
public void customizeMaxHttpHeaderSize() {
116-
bind("server.max-http-header-size=2048");
117-
Builder builder = Undertow.builder();
118-
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
119-
this.customizer.customize(factory);
120-
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
121-
"serverOptions")).getMap();
122-
assertThat(map.get(UndertowOptions.MAX_HEADER_SIZE).intValue()).isEqualTo(2048);
123-
}
124-
125-
@Test
126-
public void customMaxHttpHeaderSizeIgnoredIfNegative() {
127-
bind("server.max-http-header-size=-1");
128-
Builder builder = Undertow.builder();
129-
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
130-
this.customizer.customize(factory);
131-
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
132-
"serverOptions")).getMap();
133-
assertThat(map.contains(UndertowOptions.MAX_HEADER_SIZE)).isFalse();
134-
}
135-
136-
@Test
137-
public void customMaxHttpHeaderSizeIgnoredIfZero() {
138-
bind("server.max-http-header-size=0");
139-
Builder builder = Undertow.builder();
140-
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
141-
this.customizer.customize(factory);
142-
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
143-
"serverOptions")).getMap();
144-
assertThat(map.contains(UndertowOptions.MAX_HEADER_SIZE)).isFalse();
145-
}
146-
147-
@Test
148-
public void customConnectionTimeout() {
149-
bind("server.connection-timeout=100");
190+
private <T> T boundServerOption(Option<T> option) {
150191
Builder builder = Undertow.builder();
151192
ConfigurableUndertowWebServerFactory factory = mockFactory(builder);
152193
this.customizer.customize(factory);
153194
OptionMap map = ((OptionMap.Builder) ReflectionTestUtils.getField(builder,
154195
"serverOptions")).getMap();
155-
assertThat(map.contains(UndertowOptions.NO_REQUEST_TIMEOUT)).isTrue();
156-
assertThat(map.get(UndertowOptions.NO_REQUEST_TIMEOUT)).isEqualTo(100);
196+
return map.get(option);
157197
}
158198

159199
private ConfigurableUndertowWebServerFactory mockFactory(Builder builder) {

0 commit comments

Comments
 (0)