Skip to content

Commit

Permalink
Provide configuration for HTTP post request decoder limits rather tha…
Browse files Browse the repository at this point in the history
…n using the default limits.
  • Loading branch information
vietj committed Mar 21, 2024
1 parent d7af558 commit ea619d0
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpSer
obj.setMaxFormAttributeSize(((Number)member.getValue()).intValue());
}
break;
case "maxFormBufferedBytes":
if (member.getValue() instanceof Number) {
obj.setMaxFormBufferedBytes(((Number)member.getValue()).intValue());
}
break;
case "maxFormFields":
if (member.getValue() instanceof Number) {
obj.setMaxFormFields(((Number)member.getValue()).intValue());
}
break;
case "maxHeaderSize":
if (member.getValue() instanceof Number) {
obj.setMaxHeaderSize(((Number)member.getValue()).intValue());
Expand Down Expand Up @@ -202,6 +212,8 @@ static void toJson(HttpServerOptions obj, java.util.Map<String, Object> json) {
}
json.put("maxChunkSize", obj.getMaxChunkSize());
json.put("maxFormAttributeSize", obj.getMaxFormAttributeSize());
json.put("maxFormBufferedBytes", obj.getMaxFormBufferedBytes());
json.put("maxFormFields", obj.getMaxFormFields());
json.put("maxHeaderSize", obj.getMaxHeaderSize());
json.put("maxInitialLineLength", obj.getMaxInitialLineLength());
json.put("maxWebSocketFrameSize", obj.getMaxWebSocketFrameSize());
Expand Down
54 changes: 53 additions & 1 deletion src/main/java/io/vertx/core/http/HttpServerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,20 @@ public class HttpServerOptions extends NetServerOptions {
public static final int DEFAULT_MAX_HEADER_SIZE = 8192;

/**
* Default max length of all headers = 8192
* Default max size of a form attribute = 8192
*/
public static final int DEFAULT_MAX_FORM_ATTRIBUTE_SIZE = 8192;

/**
* Default max number of form fields = 256
*/
public static final int DEFAULT_MAX_FORM_FIELDS = 256;

/**
* Default max number buffered bytes when decoding a form = 1024
*/
public static final int DEFAULT_MAX_FORM_BUFFERED_SIZE = 1024;

/**
* Default value of whether 100-Continue should be handled automatically = {@code false}
*/
Expand Down Expand Up @@ -202,6 +212,8 @@ public class HttpServerOptions extends NetServerOptions {
private int maxInitialLineLength;
private int maxHeaderSize;
private int maxFormAttributeSize;
private int maxFormFields;
private int maxFormBufferedBytes;
private Http2Settings initialSettings;
private List<HttpVersion> alpnVersions;
private boolean http2ClearTextEnabled;
Expand Down Expand Up @@ -248,6 +260,8 @@ public HttpServerOptions(HttpServerOptions other) {
this.maxInitialLineLength = other.getMaxInitialLineLength();
this.maxHeaderSize = other.getMaxHeaderSize();
this.maxFormAttributeSize = other.getMaxFormAttributeSize();
this.maxFormFields = other.getMaxFormFields();
this.maxFormBufferedBytes = other.getMaxFormBufferedBytes();
this.initialSettings = other.initialSettings != null ? new Http2Settings(other.initialSettings) : null;
this.alpnVersions = other.alpnVersions != null ? new ArrayList<>(other.alpnVersions) : null;
this.http2ClearTextEnabled = other.http2ClearTextEnabled;
Expand Down Expand Up @@ -301,6 +315,8 @@ private void init() {
maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH;
maxHeaderSize = DEFAULT_MAX_HEADER_SIZE;
maxFormAttributeSize = DEFAULT_MAX_FORM_ATTRIBUTE_SIZE;
maxFormFields = DEFAULT_MAX_FORM_FIELDS;
maxFormBufferedBytes = DEFAULT_MAX_FORM_BUFFERED_SIZE;
initialSettings = new Http2Settings().setMaxConcurrentStreams(DEFAULT_INITIAL_SETTINGS_MAX_CONCURRENT_STREAMS);
alpnVersions = new ArrayList<>(DEFAULT_ALPN_VERSIONS);
http2ClearTextEnabled = DEFAULT_HTTP2_CLEAR_TEXT_ENABLED;
Expand Down Expand Up @@ -830,6 +846,42 @@ public HttpServerOptions setMaxFormAttributeSize(int maxSize) {
return this;
}

/**
* @return Returns the maximum number of form fields
*/
public int getMaxFormFields() {
return maxFormFields;
}

/**
* Set the maximum number of fields of a form. Set to {@code -1} to allow unlimited number of attributes
*
* @param maxFormFields the new maximum
* @return a reference to this, so the API can be used fluently
*/
public HttpServerOptions setMaxFormFields(int maxFormFields) {
this.maxFormFields = maxFormFields;
return this;
}

/**
* @return Returns the maximum number of bytes a server can buffer when decoding a form
*/
public int getMaxFormBufferedBytes() {
return maxFormBufferedBytes;
}

/**
* Set the maximum number of bytes a server can buffer when decoding a form. Set to {@code -1} to allow unlimited length
*
* @param maxFormBufferedBytes the new maximum
* @return a reference to this, so the API can be used fluently
*/
public HttpServerOptions setMaxFormBufferedBytes(int maxFormBufferedBytes) {
this.maxFormBufferedBytes = maxFormBufferedBytes;
return this;
}

/**
* @return the initial HTTP/2 connection settings
*/
Expand Down
20 changes: 15 additions & 5 deletions src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,11 @@ public HttpServerRequest setExpectMultipart(boolean expect) {
throw new IllegalStateException("Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request");
}
NettyFileUploadDataFactory factory = new NettyFileUploadDataFactory(context, this, () -> uploadHandler);
factory.setMaxLimit(conn.options.getMaxFormAttributeSize());
decoder = new HttpPostRequestDecoder(factory, request);
HttpServerOptions options = conn.options;
factory.setMaxLimit(options.getMaxFormAttributeSize());
int maxFields = options.getMaxFormFields();
int maxBufferedBytes = options.getMaxFormBufferedBytes();
decoder = new HttpPostRequestDecoder(factory, request, HttpConstants.DEFAULT_CHARSET, maxFields, maxBufferedBytes);
}
} else {
decoder = null;
Expand Down Expand Up @@ -549,7 +552,11 @@ private void onData(Buffer data) {
if (decoder != null) {
try {
decoder.offer(new DefaultHttpContent(data.getByteBuf()));
} catch (HttpPostRequestDecoder.ErrorDataDecoderException e) {
} catch (HttpPostRequestDecoder.ErrorDataDecoderException |
HttpPostRequestDecoder.TooLongFormFieldException |
HttpPostRequestDecoder.TooManyFormFieldsException e) {
decoder.destroy();
decoder = null;
handleException(e);
}
}
Expand Down Expand Up @@ -626,12 +633,15 @@ private void endDecode() {
}
}
}
} catch (HttpPostRequestDecoder.ErrorDataDecoderException e) {
} catch (HttpPostRequestDecoder.ErrorDataDecoderException |
HttpPostRequestDecoder.TooLongFormFieldException |
HttpPostRequestDecoder.TooManyFormFieldsException e) {
handleException(e);
} catch (HttpPostRequestDecoder.EndOfDataDecoderException e) {
} catch (HttpPostRequestDecoder.EndOfDataDecoderException e) {
// ignore this as it is expected
} finally {
decoder.destroy();
decoder = null;
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/main/java/io/vertx/core/http/impl/Http2ServerRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ public void handleData(Buffer data) {
if (postRequestDecoder != null) {
try {
postRequestDecoder.offer(new DefaultHttpContent(data.getByteBuf()));
} catch (Exception e) {
} catch (HttpPostRequestDecoder.ErrorDataDecoderException |
HttpPostRequestDecoder.TooLongFormFieldException |
HttpPostRequestDecoder.TooManyFormFieldsException e) {
postRequestDecoder.destroy();
postRequestDecoder = null;
handleException(e);
}
}
Expand Down Expand Up @@ -171,6 +175,7 @@ public void handleEnd(MultiMap trailers) {
handleException(e);
} finally {
postRequestDecoder.destroy();
postRequestDecoder = null;
}
}
handler = eventHandler;
Expand Down Expand Up @@ -414,8 +419,11 @@ public HttpServerRequest setExpectMultipart(boolean expect) {
stream.uri);
req.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
NettyFileUploadDataFactory factory = new NettyFileUploadDataFactory(context, this, () -> uploadHandler);
factory.setMaxLimit(stream.conn.options.getMaxFormAttributeSize());
postRequestDecoder = new HttpPostRequestDecoder(factory, req);
HttpServerOptions options = stream.conn.options;
factory.setMaxLimit(options.getMaxFormAttributeSize());
int maxFields = options.getMaxFormFields();
int maxBufferedBytes = options.getMaxFormBufferedBytes();
postRequestDecoder = new HttpPostRequestDecoder(factory, req, HttpConstants.DEFAULT_CHARSET, maxFields, maxBufferedBytes);
}
} else {
postRequestDecoder = null;
Expand Down
131 changes: 131 additions & 0 deletions src/test/java/io/vertx/core/http/HttpServerFileUploadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -523,4 +523,135 @@ public void testInvalidPostFileUpload() throws Exception {
}));
await();
}

@Test
public void testMaxFormFieldsDefaultPass() throws Exception {
testMaxFormFields(256, true);
}

@Test
public void testMaxFormFieldDefaultFail() throws Exception {
testMaxFormFields(257 + 1, false);
}

@Test
public void testMaxFormFieldsOverridePass() throws Exception {
testMaxFormFieldOverride(true);
}

@Test
public void testMaxFormFieldOverrideFail() throws Exception {
testMaxFormFieldOverride(false);
}

private void testMaxFormFieldOverride(boolean pass) throws Exception {
int newMax = 512;
server.close();
server = vertx.createHttpServer(createBaseServerOptions().setMaxFormFields(newMax));
testMaxFormFields(pass ? newMax : (newMax + 2), pass);
}

private void testMaxFormFields(int num, boolean pass) throws Exception {

server.requestHandler(req -> {
req.setExpectMultipart(true);
req.end()
.onComplete(ar -> {
req.response().setStatusCode(ar.succeeded() ? 200 : 400).end();
});
});
startServer(testAddress);

client.request(new RequestOptions(requestOptions).setMethod(HttpMethod.POST)).onComplete(onSuccess(req -> {
req.setChunked(true);
req.putHeader("content-type", "application/x-www-form-urlencoded");
StringBuilder sb = new StringBuilder();
for (int i = 0;i < num;i++) {
if (i > 0) {
sb.append('&');
}
sb.append("a").append(i).append("=").append("b");
}
req.write(sb.toString());
vertx.setTimer(10, id -> {
req.end();
});
req
.response()
.compose(resp -> {
if (pass) {
assertEquals(200, resp.statusCode());
} else {
assertEquals(400, resp.statusCode());
}
return resp.end();
}).onComplete(onSuccess(v -> testComplete()));
}));
await();
}

@Test
public void testFormMaxBufferedBytesDefaultPass() throws Exception {
testFormMaxBufferedBytes(1024, true);
}

@Test
public void testFormMaxBufferedBytesDefaultFail() throws Exception {
testFormMaxBufferedBytes(1025, false);
}

@Test
public void testFormMaxBufferedBytesOverridePass() throws Exception {
testFormMaxBufferedBytesOverride(true);
}

@Test
public void testFormMaxBufferedBytesOverrideFail() throws Exception {
testFormMaxBufferedBytesOverride(false);
}

private void testFormMaxBufferedBytesOverride(boolean pass) throws Exception {
int newMax = 2048;
server.close();
server = vertx.createHttpServer(createBaseServerOptions().setMaxFormBufferedBytes(newMax));
testFormMaxBufferedBytes(pass ? newMax : (newMax + 1), pass);
}

public void testFormMaxBufferedBytes(int len, boolean pass) throws Exception {

server.requestHandler(req -> {
req.setExpectMultipart(true);
req.end()
.onComplete(ar -> {
req.response().setStatusCode(ar.succeeded() ? 200 : 400).end();
});
});

startServer(testAddress);

client.request(new RequestOptions(requestOptions).setMethod(HttpMethod.POST)).onComplete(onSuccess(req -> {
req.setChunked(true);
req.putHeader("content-type", "application/x-www-form-urlencoded");
StringBuilder sb = new StringBuilder();
for (int i = 0;i < len;i++) {
sb.append("a");
}
req.write(sb.toString());
vertx.setTimer(10, id -> {
req.end("=b");
});
req
.response()
.compose(resp -> {
if (pass) {
assertEquals(200, resp.statusCode());
} else {
assertEquals(400, resp.statusCode());
}
return resp.end();
}).onComplete(onSuccess(v -> testComplete()));
}));

await();
}
}

0 comments on commit ea619d0

Please sign in to comment.