Skip to content

Commit faf842b

Browse files
committed
allow config for max body size by content-type
1 parent ccff936 commit faf842b

File tree

6 files changed

+114
-72
lines changed

6 files changed

+114
-72
lines changed

src/main/java/io/fusionauth/http/server/Configurable.java

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.nio.file.Path;
1919
import java.time.Duration;
20+
import java.util.Map;
2021

2122
import io.fusionauth.http.io.MultipartConfiguration;
2223
import io.fusionauth.http.log.LoggerFactory;
@@ -168,20 +169,6 @@ default T withLoggerFactory(LoggerFactory loggerFactory) {
168169
return (T) this;
169170
}
170171

171-
/**
172-
* The maximum size of the HTTP request body in bytes when the Content-Type is application/x-www-form-urlencoded. Defaults to 10
173-
* Megabytes.
174-
* <p>
175-
* Set this to -1 to disable this limitation.
176-
*
177-
* @param maxFormDataSize the maximum size of the HTTP request body in bytes.
178-
* @return This.
179-
*/
180-
default T withMaxFormDataSize(int maxFormDataSize) {
181-
configuration().withMaxFormDataSize(maxFormDataSize);
182-
return (T) this;
183-
}
184-
185172
/**
186173
* Sets the maximum number of pending socket connections per HTTP listener.
187174
* <p>
@@ -199,14 +186,15 @@ default T withMaxPendingSocketConnections(int maxPendingSocketConnections) {
199186
}
200187

201188
/**
202-
* Sets the maximum size of the HTTP request body. If this limit is exceeded, the connection will be closed. Defaults to 128 Megabytes.
189+
* Sets the maximum size of the HTTP request body by optionally per Content-Type. If this limit is exceeded, the connection will be closed.
190+
* Defaults to 128 Megabytes.
203191
* <p>
204192
* Set this to -1 to disable this limitation.
205193
*
206-
* @param maxRequestBodySize the maximum size in bytes for the HTTP request body
194+
* @param maxRequestBodySize a map specifying the maximum size in bytes for the HTTP request body by Content-Type
207195
* @return This.
208196
*/
209-
default T withMaxRequestBodySize(int maxRequestBodySize) {
197+
default T withMaxRequestBodySize(Map<String, Integer> maxRequestBodySize) {
210198
configuration().withMaxRequestBodySize(maxRequestBodySize);
211199
return (T) this;
212200
}

src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import java.nio.file.Path;
1919
import java.time.Duration;
2020
import java.util.ArrayList;
21+
import java.util.HashMap;
2122
import java.util.List;
23+
import java.util.Map;
2224
import java.util.Objects;
2325

2426
import io.fusionauth.http.io.MultipartConfiguration;
@@ -55,11 +57,11 @@ public class HTTPServerConfiguration implements Configurable<HTTPServerConfigura
5557

5658
private int maxBytesToDrain = 256 * 1024; // 256 Kilobytes
5759

58-
private int maxFormDataSize = 10 * 1024 * 1024; // 10 Megabytes, ideally same as MultipartConfiguration.maxRequestSize default
59-
6060
private int maxPendingSocketConnections = 250;
6161

62-
private int maxRequestBodySize = 128 * 1024 * 1024; // 128 Megabytes, must be equal to or larger than maxFormDataSize, and MultipartConfiguration.maxRequestSize
62+
private Map<String, Integer> maxRequestBodySize = Map.of(
63+
"*", 128 * 1024 * 1024, // 128 Megabytes
64+
"application/x-www-form-urlencoded", 10 * 1024 * 1024); // 10 Megabytes
6365

6466
private int maxRequestHeaderSize = 128 * 1024; // 128 Kilobytes
6567

@@ -187,14 +189,6 @@ public int getMaxBytesToDrain() {
187189
return maxBytesToDrain;
188190
}
189191

190-
/**
191-
* @return the maximum size of the HTTP request body in bytes when the HTTP server process a Content-Type of
192-
* application/x-www-form-urlencoded. Defaults to 10 Megabytes.
193-
*/
194-
public int getMaxFormDataSize() {
195-
return maxFormDataSize;
196-
}
197-
198192
/**
199193
* The maximum number of pending socket connections per HTTP listener.
200194
* <p>
@@ -209,14 +203,16 @@ public int getMaxPendingSocketConnections() {
209203
}
210204

211205
/**
212-
* The maximum size in bytes of the HTTP request body. This configuration excludes the size of the HTTP request header.
206+
* The map that specifies the maximum size in bytes of the HTTP request body by Content-Type. This configuration excludes the size of the
207+
* HTTP request header.
213208
* <p>
214-
* This configuration will affect all requests regardless of Content-Type and is intended to be a fail-safe for unexpected large
215-
* requests.
209+
* The returned map is keyed by Content-Type, and willy contain a default value identified by '*', and may optionally return content type
210+
* values with a wild card '*' as the subtype.
216211
*
217-
* @return the maximum size in bytes of the HTTP request body. Defaults to 128 Megabytes.
212+
* @return the map keyed by Content-Type indicating the maximum size in bytes of the HTTP request body. Defaults to 128 Megabytes as a
213+
* default, and 10 Megabytes for application/x-www-form-urlencoded.
218214
*/
219-
public int getMaxRequestBodySize() {
215+
public Map<String, Integer> getMaxRequestBodySize() {
220216
return maxRequestBodySize;
221217
}
222218

@@ -463,19 +459,6 @@ public HTTPServerConfiguration withLoggerFactory(LoggerFactory loggerFactory) {
463459
return this;
464460
}
465461

466-
/**
467-
* {@inheritDoc}
468-
*/
469-
@Override
470-
public HTTPServerConfiguration withMaxFormDataSize(int maxFormDataSize) {
471-
if (maxFormDataSize != -1 && maxFormDataSize <= 0) {
472-
throw new IllegalArgumentException("The maximum form data size must be greater than 0. Set to -1 to disable this limitation.");
473-
}
474-
475-
this.maxFormDataSize = maxFormDataSize;
476-
return this;
477-
}
478-
479462
/**
480463
* {@inheritDoc}
481464
*/
@@ -493,9 +476,22 @@ public HTTPServerConfiguration withMaxPendingSocketConnections(int maxPendingSoc
493476
* {@inheritDoc}
494477
*/
495478
@Override
496-
public HTTPServerConfiguration withMaxRequestBodySize(int maxRequestBodySize) {
497-
if (maxRequestBodySize != -1 && maxRequestBodySize <= 0) {
498-
throw new IllegalArgumentException("The maximum request body size must be greater than 0. Set to -1 to disable this limitation.");
479+
public HTTPServerConfiguration withMaxRequestBodySize(Map<String, Integer> maxRequestBodySize) {
480+
Objects.requireNonNull(maxRequestBodySize, "You cannot set the maximum request body size map to null");
481+
for (String contentType : maxRequestBodySize.keySet()) {
482+
Objects.requireNonNull(contentType, "You cannot specify a null value for content type");
483+
Integer maxSize = maxRequestBodySize.get(contentType);
484+
Objects.requireNonNull(maxSize, "You may not specify a null value for the maximum request body size");
485+
if (maxSize != -1 && maxSize <= 0) {
486+
throw new IllegalArgumentException("The maximum request body size must be greater than 0 for [" + contentType + "]. Set to -1 to disable this limitation.");
487+
}
488+
}
489+
490+
// Keep existing default if one was not provided.
491+
if (!maxRequestBodySize.containsKey("*")) {
492+
Map<String, Integer> copy = new HashMap<>(maxRequestBodySize);
493+
copy.put("*", maxRequestBodySize.get("*"));
494+
maxRequestBodySize = copy;
499495
}
500496

501497
this.maxRequestBodySize = maxRequestBodySize;
@@ -593,8 +589,6 @@ public HTTPServerConfiguration withMultipartBufferSize(int multipartBufferSize)
593589
return this;
594590
}
595591

596-
// TODO : Review coments for constraints for multi-part, etc.
597-
598592
@Override
599593
public HTTPServerConfiguration withMultipartConfiguration(MultipartConfiguration multipartStreamConfiguration) {
600594
Objects.requireNonNull(multipartStreamConfiguration, "You cannot set the multipart stream configuration to null");

src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import io.fusionauth.http.HTTPProcessingException;
2626
import io.fusionauth.http.HTTPValues;
2727
import io.fusionauth.http.HTTPValues.Connections;
28-
import io.fusionauth.http.HTTPValues.ContentTypes;
2928
import io.fusionauth.http.HTTPValues.Headers;
3029
import io.fusionauth.http.HTTPValues.Protocols;
3130
import io.fusionauth.http.ParseException;
@@ -145,7 +144,7 @@ public void run() {
145144
instrumenter.acceptedRequest();
146145
}
147146

148-
int maximumContentLength = getMaximumContentLength(request);
147+
int maximumContentLength = HTTPTools.getMaxRequestBodySize(request.getContentType(), configuration.getMaxRequestBodySize());
149148
httpInputStream = new HTTPInputStream(configuration, request, inputStream, maximumContentLength);
150149
request.setInputStream(httpInputStream);
151150

@@ -332,19 +331,6 @@ private void closeSocketOnly(CloseSocketReason reason) {
332331
}
333332
}
334333

335-
private int getMaximumContentLength(HTTPRequest request) {
336-
var maximumContentLength = -1;
337-
if (ContentTypes.Form.equalsIgnoreCase(request.getContentType())) {
338-
maximumContentLength = configuration.getMaxFormDataSize();
339-
}
340-
341-
if (maximumContentLength == -1) {
342-
maximumContentLength = configuration.getMaxRequestBodySize();
343-
}
344-
345-
return maximumContentLength;
346-
}
347-
348334
private boolean handleExpectContinue(HTTPRequest request) throws IOException {
349335
var expectResponse = new HTTPResponse();
350336
configuration.getExpectValidator().validate(request, expectResponse);

src/main/java/io/fusionauth/http/util/HTTPTools.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,37 @@
4545
public final class HTTPTools {
4646
private static Logger logger;
4747

48+
/**
49+
* Return the maximum request body size for the requested content type.
50+
*
51+
* @param contentType the content-type of the request
52+
* @param maxRequestBodySize the maximum request size configuration
53+
* @return the maximum request size, or -1 if no limit should be enforced.
54+
*/
55+
public static int getMaxRequestBodySize(String contentType, Map<String, Integer> maxRequestBodySize) {
56+
if (contentType == null) {
57+
return maxRequestBodySize.get("*");
58+
}
59+
60+
// Exact match
61+
Integer maximumSize = maxRequestBodySize.get(contentType);
62+
if (maximumSize != null) {
63+
return maximumSize;
64+
}
65+
66+
// Ignore subtype by replacing it with '*'. RFC 1341 says a subtype is required which means each Content-Type must contain a /.
67+
// - But be more defensive, and account for a case where no subtype has been defined.
68+
int index = contentType.indexOf('/');
69+
if (index != -1) {
70+
maximumSize = maxRequestBodySize.get(contentType.substring(0, index) + "/*");
71+
}
72+
73+
// RFC 1341 indicates subtypes cannot be nested. So if we do not yet have a match, use the default key '*'.
74+
return maximumSize != null
75+
? maximumSize
76+
: maxRequestBodySize.get("*");
77+
}
78+
4879
/**
4980
* Statically sets up the logger, mostly for trace logging.
5081
*
@@ -343,7 +374,7 @@ public static void parseRequestPreamble(PushbackInputStream inputStream, int max
343374

344375
// index is the number of bytes we processed as part of the preamble
345376
premableLength += index;
346-
if (maxRequestHeaderSize !=-1 && premableLength > maxRequestHeaderSize) {
377+
if (maxRequestHeaderSize != -1 && premableLength > maxRequestHeaderSize) {
347378
throw new RequestHeadersTooLargeException(maxRequestHeaderSize, "The maximum size of the request header has been exceeded. The maximum size is [" + maxRequestHeaderSize + "] bytes.");
348379
}
349380
}

src/test/java/io/fusionauth/http/FormDataTest.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Map;
2727
import java.util.function.Consumer;
2828

29+
import io.fusionauth.http.HTTPValues.ContentTypes;
2930
import io.fusionauth.http.HTTPValues.Headers;
3031
import io.fusionauth.http.server.HTTPHandler;
3132
import io.fusionauth.http.server.HTTPServer;
@@ -60,7 +61,7 @@ public void post_server_configuration_max_form_data(String scheme, boolean chunk
6061
// Account for the equals size and a separator of & except for the first value
6162
// - This should mean we have just exactly the right size of configuration for this request body
6263
// Config is [180,223]
63-
.withConfiguration(config -> config.withMaxFormDataSize((4096 * 10) + (4096 * 32) + (4096 * 2) - 1))
64+
.withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form, (4096 * 10) + (4096 * 32) + (4096 * 2) - 1)))
6465
.expectResponse("""
6566
HTTP/1.1 200 \r
6667
connection: keep-alive\r
@@ -76,7 +77,7 @@ public void post_server_configuration_max_form_data(String scheme, boolean chunk
7677
.withBodyParameterCount(42 * 1024) // 43,008
7778
.withBodyParameterSize(128)
7879
// 4k * 33 > 128k
79-
.withConfiguration(config -> config.withMaxFormDataSize(128 * 1024))
80+
.withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form, 128 * 1024)))
8081
.expectResponse("""
8182
HTTP/1.1 413 \r
8283
connection: close\r
@@ -88,10 +89,10 @@ public void post_server_configuration_max_form_data(String scheme, boolean chunk
8889
// Large, but max size has been disabled
8990
withScheme(scheme)
9091
.withChunked(chunked)
91-
.withBodyParameterCount(42 * 1024) // 131,072, 131,072
92+
.withBodyParameterCount(42 * 1024) // 43,008
9293
.withBodyParameterSize(128)
9394
// Disable the limit
94-
.withConfiguration(config -> config.withMaxFormDataSize(-1))
95+
.withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form, -1)))
9596
.expectResponse("""
9697
HTTP/1.1 200 \r
9798
connection: keep-alive\r
@@ -100,6 +101,21 @@ public void post_server_configuration_max_form_data(String scheme, boolean chunk
100101
\r
101102
{"version":"42"}""")
102103
.expectNoExceptionOnWrite();
104+
105+
// Large, enforce using default w/out a specific configuration for application/x-www-form-urlencoded
106+
withScheme(scheme)
107+
.withChunked(chunked)
108+
.withBodyParameterCount(42 * 1024) // 131,072, 131,072
109+
.withBodyParameterSize(128)
110+
// Disable the limit
111+
.withConfiguration(config -> config.withMaxRequestBodySize(Map.of("*", 128 * 1024)))
112+
.expectResponse("""
113+
HTTP/1.1 413 \r
114+
connection: close\r
115+
content-length: 0\r
116+
\r
117+
""")
118+
.expectExceptionOnWrite(SocketException.class);
103119
}
104120

105121
@Test(dataProvider = "schemes")
@@ -313,7 +329,7 @@ public Builder withChunked(boolean chunked) {
313329
return this;
314330
}
315331

316-
public Builder withConfiguration(Consumer<HTTPServerConfiguration> configuration) throws Exception {
332+
public Builder withConfiguration(Consumer<HTTPServerConfiguration> configuration) {
317333
this.configuration = configuration;
318334
return this;
319335
}

src/test/java/io/fusionauth/http/util/HTTPToolsTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,29 @@
4242
*/
4343
@Test
4444
public class HTTPToolsTest {
45+
@Test
46+
public void getMaxRequestBodySize() {
47+
var configuration = Map.of(
48+
"*", 1,
49+
"application/*", 2,
50+
"application/json", 3,
51+
"application/x-www-form-urlencoded", 4,
52+
"multipart/form-data", 5,
53+
"text/*", 6,
54+
"text/html", 7
55+
);
56+
57+
assertMaxConfiguredSize(null, 1, configuration);
58+
assertMaxConfiguredSize("application/json", 3, configuration);
59+
assertMaxConfiguredSize("application/json-patch+json", 2, configuration);
60+
assertMaxConfiguredSize("application/octet-stream", 2, configuration);
61+
assertMaxConfiguredSize("application/pdf", 2, configuration);
62+
assertMaxConfiguredSize("application/x-www-form-urlencoded", 4, configuration);
63+
assertMaxConfiguredSize("multipart/form-data", 5, configuration);
64+
assertMaxConfiguredSize("text/css", 6, configuration);
65+
assertMaxConfiguredSize("text/html", 7, configuration);
66+
}
67+
4568
@Test
4669
public void parseEncodedData() {
4770
// Happy path
@@ -240,6 +263,10 @@ private void assertHexValue(String s, String expected, Charset charset) {
240263
assertEquals(hex(s.getBytes(charset)), trimmed);
241264
}
242265

266+
private void assertMaxConfiguredSize(String contentType, int maximumSize, Map<String, Integer> maxRequestBodySize) {
267+
assertEquals(maximumSize, HTTPTools.getMaxRequestBodySize(contentType, maxRequestBodySize));
268+
}
269+
243270
private String hex(byte[] bytes) {
244271
List<String> result = new ArrayList<>();
245272
for (byte b : bytes) {

0 commit comments

Comments
 (0)