diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java index d071a33698b4ce..af2fa0ff05c17c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java @@ -35,6 +35,7 @@ public Supplier runtimeConfiguration(RuntimeValue> fileContentTypes; +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java index 216da2f1965f6e..ef77e6a026c52c 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java @@ -53,6 +53,7 @@ public class MultiPartParserDefinition implements FormParserFactory.ParserDefini private long maxAttributeSize = 2048; private long maxEntitySize = -1; + private List fileContentTypes; public MultiPartParserDefinition(Supplier executorSupplier) { this.executorSupplier = executorSupplier; @@ -152,6 +153,15 @@ public MultiPartParserDefinition setMaxEntitySize(long maxEntitySize) { return this; } + public List getFileContentTypes() { + return fileContentTypes; + } + + public MultiPartParserDefinition setFileContentTypes(List fileContentTypes) { + this.fileContentTypes = fileContentTypes; + return this; + } + private final class MultiPartUploadHandler implements FormDataParser, MultipartParser.PartHandler { private final ResteasyReactiveRequestContext exchange; @@ -236,7 +246,8 @@ public void beginPart(final CaseInsensitiveMap headers) { if (disposition.startsWith("form-data")) { currentName = HeaderUtil.extractQuotedValueFromHeader(disposition, "name"); fileName = HeaderUtil.extractQuotedValueFromHeaderWithEncoding(disposition, "filename"); - if (fileName != null && fileSizeThreshold == 0) { + String contentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + if ((fileName != null || isFileContentType(contentType)) && fileSizeThreshold == 0) { try { if (tempFileLocation != null) { Files.createDirectories(tempFileLocation); @@ -254,6 +265,14 @@ public void beginPart(final CaseInsensitiveMap headers) { } } + private boolean isFileContentType(String contentType) { + if (contentType == null || fileContentTypes == null) { + return false; + } + + return fileContentTypes.contains(contentType); + } + @Override public void data(final ByteBuffer buffer) throws IOException { this.currentFileSize += buffer.remaining(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java index b7f12b58df2692..196d76632a83a8 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java @@ -46,6 +46,7 @@ public void configure(RuntimeConfiguration configuration) { .setMaxAttributeSize(configuration.limits().maxFormAttributeSize()) .setMaxEntitySize(configuration.limits().maxBodySize().orElse(-1L)) .setDeleteUploadsOnEnd(configuration.body().deleteUploadedFilesOnEnd()) + .setFileContentTypes(configuration.body().multiPart().fileContentTypes()) .setDefaultCharset(configuration.body().defaultCharset().name()) .setTempFileLocation(Path.of(configuration.body().uploadsDirectory()))) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java index 6fe9c4a290ab18..958c6e55b7f8fe 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java @@ -2,6 +2,7 @@ import java.nio.charset.Charset; import java.time.Duration; +import java.util.List; import java.util.Optional; public class DefaultRuntimeConfiguration implements RuntimeConfiguration { @@ -10,9 +11,16 @@ public class DefaultRuntimeConfiguration implements RuntimeConfiguration { private final Limits limits; public DefaultRuntimeConfiguration(Duration readTimeout, boolean deleteUploadedFilesOnEnd, String uploadsDirectory, - Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { + List fileContentTypes, Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { this.readTimeout = readTimeout; body = new Body() { + Body.MultiPart multiPart = new Body.MultiPart() { + @Override + public List fileContentTypes() { + return fileContentTypes; + } + }; + @Override public boolean deleteUploadedFilesOnEnd() { return deleteUploadedFilesOnEnd; @@ -27,6 +35,11 @@ public String uploadsDirectory() { public Charset defaultCharset() { return defaultCharset; } + + @Override + public MultiPart multiPart() { + return multiPart; + } }; limits = new Limits() { @Override diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java index 5bdd06534a5371..6ccfdcee9dd508 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java @@ -2,6 +2,7 @@ import java.nio.charset.Charset; import java.time.Duration; +import java.util.List; import java.util.Optional; public interface RuntimeConfiguration { @@ -19,6 +20,12 @@ interface Body { String uploadsDirectory(); Charset defaultCharset(); + + MultiPart multiPart(); + + interface MultiPart { + List fileContentTypes(); + } } interface Limits { diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java index 92d770aeb910ae..b996daf921ddce 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java @@ -121,6 +121,7 @@ public boolean isBlockingAllowed() { static Vertx vertx; static ExecutorService executor; boolean deleteUploadedFilesOnEnd = true; + List fileContentTypes; Path uploadPath; private List> scanCustomizers = new ArrayList<>(); @@ -149,6 +150,11 @@ public ResteasyReactiveUnitTest setDeleteUploadedFilesOnEnd(boolean deleteUpload return this; } + public ResteasyReactiveUnitTest setFileContentTypes(List fileContentTypes) { + this.fileContentTypes = fileContentTypes; + return this; + } + public ResteasyReactiveUnitTest setUploadPath(Path uploadPath) { this.uploadPath = uploadPath; return this; @@ -391,7 +397,7 @@ public Thread newThread(Runnable r) { DefaultRuntimeConfiguration runtimeConfiguration = new DefaultRuntimeConfiguration(Duration.ofMinutes(1), deleteUploadedFilesOnEnd, uploadPath != null ? uploadPath.toAbsolutePath().toString() : System.getProperty("java.io.tmpdir"), - defaultCharset, Optional.empty(), maxFormAttributeSize); + fileContentTypes, defaultCharset, Optional.empty(), maxFormAttributeSize); ResteasyReactiveDeploymentManager.RunnableApplication application = prepared.createApplication(runtimeConfiguration, new VertxRequestContextFactory(), executor); fieldInjectionSupport.runtimeInit(testClassLoader, application.getDeployment()); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java new file mode 100644 index 00000000000000..85157244a79b00 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java @@ -0,0 +1,86 @@ +package org.jboss.resteasy.reactive.server.vertx.test.multipart; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Supplier; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +import org.jboss.resteasy.reactive.server.vertx.test.multipart.other.OtherPackageFormDataBase; +import org.jboss.shrinkwrap.api.ShrinkWrap; +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.restassured.RestAssured; + +public class MultipartFileContentTypeTest extends AbstractMultipartTest { + + private static final Path uploadDir = Paths.get("file-uploads"); + + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setDeleteUploadedFilesOnEnd(false) + .setUploadPath(uploadDir) + .setFileContentTypes(List.of(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_SVG_XML)) + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(FormDataBase.class, OtherPackageFormDataBase.class, FormData.class, Status.class, + OtherFormData.class, FormDataSameFileName.class, + OtherFormDataBase.class, + MultipartResource.class, OtherMultipartResource.class); + } + + }); + + private final File FILE = new File("./src/test/resources/test.html"); + + @BeforeEach + public void assertEmptyUploads() { + Assertions.assertTrue(isDirectoryEmpty(uploadDir)); + } + + @AfterEach + public void clearDirectory() { + clearDirectory(uploadDir); + } + + @Test + public void testFilePartWithExpectedContentType() throws IOException { + RestAssured.given() + .multiPart("octetStream", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_OCTET_STREAM) + .multiPart("svgXml", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_SVG_XML) + .accept("text/plain") + .when() + .post("/multipart/optional") + .then() + .statusCode(200); + + // ensure that the 2 uploaded files where created on disk + Assertions.assertEquals(2, uploadDir.toFile().listFiles().length); + } + + @Test + public void testFilePartWithUnexpectedContentType() throws IOException { + RestAssured.given() + .multiPart("xmlFile", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_XML) + .accept("text/plain") + .when() + .post("/multipart/optional") + .then() + .statusCode(200); + + // ensure that no files are created as the content-type is not supported as a file part + Assertions.assertEquals(0, uploadDir.toFile().listFiles().length); + } +}