diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/AbstractMultipartTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/AbstractMultipartTest.java new file mode 100644 index 0000000000000..86348aa16e4b8 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/AbstractMultipartTest.java @@ -0,0 +1,43 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import static org.awaitility.Awaitility.await; + +import java.io.File; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +abstract class AbstractMultipartTest { + + protected boolean isDirectoryEmpty(Path uploadDir) { + File[] files = uploadDir.toFile().listFiles(); + if (files == null) { + return true; + } + return files.length == 0; + } + + protected void clearDirectory(Path uploadDir) { + File[] files = uploadDir.toFile().listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (!file.isDirectory()) { + file.delete(); + } + } + } + + protected void awaitUploadDirectoryToEmpty(Path uploadDir) { + await().atMost(10, TimeUnit.SECONDS) + .pollInterval(Duration.ofSeconds(1)) + .until(new Callable() { + @Override + public Boolean call() { + return isDirectoryEmpty(uploadDir); + } + }); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java new file mode 100644 index 0000000000000..5489e49172b3f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java @@ -0,0 +1,89 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.function.Supplier; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.http.ContentType; +import io.vertx.core.http.HttpServerOptions; + +public class LargerThanDefaultFormAttributeMultipartFormInputTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, Data.class) + .addAsResource(new StringAsset( + "quarkus.http.limits.max-form-attribute-size=4K"), + "application.properties"); + } + }); + + private final File FILE = new File("./src/test/resources/larger-than-default-form-attribute.txt"); + + @Test + public void test() throws IOException { + String fileContents = new String(Files.readAllBytes(FILE.toPath()), StandardCharsets.UTF_8); + Assertions.assertTrue(fileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); + given() + .multiPart("text", fileContents) + .accept("text/plain") + .when() + .post("/test") + .then() + .statusCode(200) + .contentType(ContentType.TEXT) + .body(equalTo(fileContents)); + } + + @Path("/test") + public static class Resource { + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + public String hello(@MultipartForm Data data) { + return data.getText(); + } + } + + public static class Data { + @FormParam("text") + @PartType("text/plain") + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java index 7d8e32aa2a098..a75ec72d748cb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java @@ -23,7 +23,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; -public class MultipartInputTest { +public class MultipartInputTest extends AbstractMultipartTest { private static final Path uploadDir = Paths.get("file-uploads"); @@ -52,16 +52,12 @@ public JavaArchive get() { @BeforeEach public void assertEmptyUploads() { - Assertions.assertEquals(0, uploadDir.toFile().listFiles().length); + Assertions.assertTrue(isDirectoryEmpty(uploadDir)); } @AfterEach public void clearDirectory() { - for (File file : uploadDir.toFile().listFiles()) { - if (!file.isDirectory()) { - file.delete(); - } - } + clearDirectory(uploadDir); } @Test diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java new file mode 100644 index 0000000000000..6699beab8c705 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java @@ -0,0 +1,103 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import static io.restassured.RestAssured.given; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.function.Supplier; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.resteasy.reactive.server.test.multipart.other.OtherPackageFormDataBase; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.core.http.HttpServerOptions; + +public class TooLargeFormAttributeMultipartFormInputTest extends AbstractMultipartTest { + + private static final java.nio.file.Path uploadDir = Paths.get("file-uploads"); + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, Status.class, FormDataBase.class, OtherPackageFormDataBase.class, + FormData.class) + .addAsResource(new StringAsset( + // keep the files around so we can assert the outcome + "quarkus.http.body.delete-uploaded-files-on-end=false\nquarkus.http.body.uploads-directory=" + + uploadDir.toString() + "\n"), + "application.properties"); + } + }); + + private final File FORM_ATTR_SOURCE_FILE = new File("./src/test/resources/larger-than-default-form-attribute.txt"); + private final File HTML_FILE = new File("./src/test/resources/test.html"); + private final File XML_FILE = new File("./src/test/resources/test.html"); + private final File TXT_FILE = new File("./src/test/resources/lorem.txt"); + + @BeforeEach + public void assertEmptyUploads() { + Assertions.assertTrue(isDirectoryEmpty(uploadDir)); + } + + @AfterEach + public void clearDirectory() { + clearDirectory(uploadDir); + } + + @Test + public void test() throws IOException { + String formAttrSourceFileContents = new String(Files.readAllBytes(FORM_ATTR_SOURCE_FILE.toPath()), + StandardCharsets.UTF_8); + Assertions.assertTrue(formAttrSourceFileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); + given() + .multiPart("active", "true") + .multiPart("num", "25") + .multiPart("status", "WORKING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .multiPart("name", formAttrSourceFileContents) + .accept("text/plain") + .when() + .post("/test") + .then() + .statusCode(413); + + // ensure that no files where created on disk + // as RESTEasy Reactive doesn't wait for the files to be deleted before returning the HTTP response, + // we need to wait in the test + awaitUploadDirectoryToEmpty(uploadDir); + } + + @Path("/test") + public static class Resource { + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + public String hello(@MultipartForm FormData data) { + return data.getName(); + } + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/resources/larger-than-default-form-attribute.txt b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/resources/larger-than-default-form-attribute.txt new file mode 100644 index 0000000000000..a3f5f3faa854b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/resources/larger-than-default-form-attribute.txt @@ -0,0 +1,143 @@ +File-Date: 2018-04-23 +%% +Type: language +Subtag: aa +Description: Afar +Added: 2005-10-16 +%% +Type: language +Subtag: ab +Description: Abkhazian +Added: 2005-10-16 +Suppress-Script: Cyrl +%% +Type: language +Subtag: ae +Description: Avestan +Added: 2005-10-16 +%% +Type: language +Subtag: af +Description: Afrikaans +Added: 2005-10-16 +Suppress-Script: Latn +%% +Type: language +Subtag: ak +Description: Akan +Added: 2005-10-16 +Scope: macrolanguage +%% +Type: language +Subtag: am +Description: Amharic +Added: 2005-10-16 +Suppress-Script: Ethi +%% +Type: language +Subtag: an +Description: Aragonese +Added: 2005-10-16 +%% +Type: language +Subtag: ar +Description: Arabic +Added: 2005-10-16 +Suppress-Script: Arab +Scope: macrolanguage +%% +Type: language +Subtag: as +Description: Assamese +Added: 2005-10-16 +Suppress-Script: Beng +%% +Type: language +Subtag: av +Description: Avaric +Added: 2005-10-16 +%% +Type: language +Subtag: ay +Description: Aymara +Added: 2005-10-16 +Suppress-Script: Latn +Scope: macrolanguage +%% +Type: language +Subtag: az +Description: Azerbaijani +Added: 2005-10-16 +Scope: macrolanguage +%% +Type: language +Subtag: ba +Description: Bashkir +Added: 2005-10-16 +%% +Type: language +Subtag: be +Description: Belarusian +Added: 2005-10-16 +Suppress-Script: Cyrl +%% +Type: language +Subtag: bg +Description: Bulgarian +Added: 2005-10-16 +Suppress-Script: Cyrl +%% +Type: language +Subtag: bh +Description: Bihari languages +Added: 2005-10-16 +Scope: collection +%% +Type: language +Subtag: bi +Description: Bislama +Added: 2005-10-16 +%% +Type: language +Subtag: bm +Description: Bambara +Added: 2005-10-16 +%% +Type: language +Subtag: bn +Description: Bengali +Description: Bangla +Added: 2005-10-16 +Suppress-Script: Beng +%% +Type: language +Subtag: bo +Description: Tibetan +Added: 2005-10-16 +%% +Type: language +Subtag: br +Description: Breton +Added: 2005-10-16 +%% +Type: language +Subtag: bs +Description: Bosnian +Added: 2005-10-16 +Suppress-Script: Latn +Macrolanguage: sh +%% +Type: language +Subtag: ca +Description: Catalan +Description: Valencian +Added: 2005-10-16 +Suppress-Script: Latn +%% +Type: language +Subtag: ce +Description: +%% +Type: language +Subtag: ce +Description: diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/MultipartFormHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/MultipartFormHandler.java index f18c90c0005fa..97488c5c0da95 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/MultipartFormHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/MultipartFormHandler.java @@ -21,6 +21,7 @@ import org.jboss.resteasy.reactive.server.spi.RuntimeConfigurableServerRestHandler; import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; +import io.netty.handler.codec.DecoderException; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.file.FileSystem; @@ -116,6 +117,15 @@ public MultipartFormVertxHandler(ResteasyReactiveRequestContext rrContext, Strin Set fileUploads = context.fileUploads(); context.request().setExpectMultipart(true); + context.request().exceptionHandler(new Handler() { + @Override + public void handle(Throwable t) { + cancelUploads(); + rrContext.resume(new WebApplicationException( + (t instanceof DecoderException) ? Response.Status.REQUEST_ENTITY_TOO_LARGE + : Response.Status.INTERNAL_SERVER_ERROR)); + } + }); context.request().uploadHandler(new Handler() { @Override public void handle(HttpServerFileUpload upload) { @@ -152,6 +162,20 @@ public void handle(Throwable ignored) { }); } + private void cancelUploads() { + for (FileUpload fileUpload : context.fileUploads()) { + FileSystem fileSystem = context.vertx().fileSystem(); + if (!fileUpload.cancel()) { + String uploadedFileName = fileUpload.uploadedFileName(); + fileSystem.delete(uploadedFileName, deleteResult -> { + if (deleteResult.failed()) { + LOG.warn("Delete of uploaded file failed: " + uploadedFileName, deleteResult.cause()); + } + }); + } + } + } + @Override public void handle(Buffer buff) { if (failed) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerLimitsConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerLimitsConfig.java index b4a90829267d3..65dee1c842cbe 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerLimitsConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerLimitsConfig.java @@ -32,4 +32,10 @@ public class ServerLimitsConfig { @ConfigItem(defaultValue = "4096") public int maxInitialLineLength; + /** + * The maximum length of a form attribute. + */ + @ConfigItem(defaultValue = "2048") + public MemorySize maxFormAttributeSize; + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 9fd08338ad139..52f8e98faccf0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -593,6 +593,7 @@ private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeC } serverOptions.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); serverOptions.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + serverOptions.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); setIdleTimeout(httpConfiguration, serverOptions); if (certFile.isPresent() && keyFile.isPresent()) { @@ -725,6 +726,7 @@ private static HttpServerOptions createHttpServerOptions(HttpConfiguration httpC setIdleTimeout(httpConfiguration, options); options.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); options.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + options.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); options.setWebSocketSubProtocols(websocketSubProtocols); options.setReusePort(httpConfiguration.soReusePort); options.setTcpQuickAck(httpConfiguration.tcpQuickAck); @@ -745,6 +747,7 @@ private static HttpServerOptions createDomainSocketOptions(HttpConfiguration htt setIdleTimeout(httpConfiguration, options); options.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); options.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + options.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); options.setWebSocketSubProtocols(websocketSubProtocols); return options; }