diff --git a/src/main/generated/io/vertx/core/http/HttpServerOptionsConverter.java b/src/main/generated/io/vertx/core/http/HttpServerOptionsConverter.java index 88441aa872c..068a72a493f 100644 --- a/src/main/generated/io/vertx/core/http/HttpServerOptionsConverter.java +++ b/src/main/generated/io/vertx/core/http/HttpServerOptionsConverter.java @@ -100,6 +100,16 @@ static void fromJson(Iterable> 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()); @@ -202,6 +212,8 @@ static void toJson(HttpServerOptions obj, java.util.Map 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()); diff --git a/src/main/java/io/vertx/core/http/HttpServerOptions.java b/src/main/java/io/vertx/core/http/HttpServerOptions.java index 7b0f2296bb9..345c804f862 100755 --- a/src/main/java/io/vertx/core/http/HttpServerOptions.java +++ b/src/main/java/io/vertx/core/http/HttpServerOptions.java @@ -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} */ @@ -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 alpnVersions; private boolean http2ClearTextEnabled; @@ -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; @@ -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; @@ -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 */ diff --git a/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java b/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java index b72c73389d9..f485024403b 100644 --- a/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java +++ b/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java @@ -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; @@ -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); } } @@ -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; } } diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerRequest.java b/src/main/java/io/vertx/core/http/impl/Http2ServerRequest.java index f8d40c22bb1..58de3550f08 100644 --- a/src/main/java/io/vertx/core/http/impl/Http2ServerRequest.java +++ b/src/main/java/io/vertx/core/http/impl/Http2ServerRequest.java @@ -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); } } @@ -171,6 +175,7 @@ public void handleEnd(MultiMap trailers) { handleException(e); } finally { postRequestDecoder.destroy(); + postRequestDecoder = null; } } handler = eventHandler; @@ -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; diff --git a/src/test/java/io/vertx/core/http/HttpServerFileUploadTest.java b/src/test/java/io/vertx/core/http/HttpServerFileUploadTest.java index dd5d62c2078..2d79e5e2520 100644 --- a/src/test/java/io/vertx/core/http/HttpServerFileUploadTest.java +++ b/src/test/java/io/vertx/core/http/HttpServerFileUploadTest.java @@ -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(); + } }