Skip to content

Commit

Permalink
Handling a multipart request part as a file based on the content-type
Browse files Browse the repository at this point in the history
Closes quarkusio#29725

(cherry picked from commit f54a303)
  • Loading branch information
pedroigor authored and gsmet committed Dec 20, 2022
1 parent 615d508 commit f06081f
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public Supplier<RuntimeConfiguration> runtimeConfiguration(RuntimeValue<Deployme

RuntimeConfiguration runtimeConfiguration = new DefaultRuntimeConfiguration(httpConf.readTimeout,
httpConf.body.deleteUploadedFilesOnEnd, httpConf.body.uploadsDirectory,
httpConf.body.multipart.fileContentTypes.orElse(null),
runtimeConf.multipart.inputPart.defaultCharset, maxBodySize,
httpConf.limits.maxFormAttributeSize.asLongValue());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,10 @@ public class BodyConfig {
*/
@ConfigItem
public boolean preallocateBodyBuffer;

/**
* HTTP multipart request related settings
*/
@ConfigItem
public MultiPartConfig multipart;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.vertx.http.runtime;

import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConvertWith;
import io.quarkus.runtime.configuration.TrimmedStringConverter;

/**
* A {@link ConfigGroup} for the settings related to HTTP multipart request handling.
*/
@ConfigGroup
public class MultiPartConfig {

/**
* A list of {@code ContentType} to indicate whether a given multipart field should be handled as a file part.
*/
@ConfigItem
@ConvertWith(TrimmedStringConverter.class)
public Optional<List<String>> fileContentTypes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class MultiPartParserDefinition implements FormParserFactory.ParserDefini

private long maxAttributeSize = 2048;
private long maxEntitySize = -1;
private List<String> fileContentTypes;

public MultiPartParserDefinition(Supplier<Executor> executorSupplier) {
this.executorSupplier = executorSupplier;
Expand Down Expand Up @@ -152,6 +153,15 @@ public MultiPartParserDefinition setMaxEntitySize(long maxEntitySize) {
return this;
}

public List<String> getFileContentTypes() {
return fileContentTypes;
}

public MultiPartParserDefinition setFileContentTypes(List<String> fileContentTypes) {
this.fileContentTypes = fileContentTypes;
return this;
}

private final class MultiPartUploadHandler implements FormDataParser, MultipartParser.PartHandler {

private final ResteasyReactiveRequestContext exchange;
Expand Down Expand Up @@ -236,7 +246,8 @@ public void beginPart(final CaseInsensitiveMap<String> 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);
Expand All @@ -254,6 +265,14 @@ public void beginPart(final CaseInsensitiveMap<String> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -10,9 +11,16 @@ public class DefaultRuntimeConfiguration implements RuntimeConfiguration {
private final Limits limits;

public DefaultRuntimeConfiguration(Duration readTimeout, boolean deleteUploadedFilesOnEnd, String uploadsDirectory,
Charset defaultCharset, Optional<Long> maxBodySize, long maxFormAttributeSize) {
List<String> fileContentTypes, Charset defaultCharset, Optional<Long> maxBodySize, long maxFormAttributeSize) {
this.readTimeout = readTimeout;
body = new Body() {
Body.MultiPart multiPart = new Body.MultiPart() {
@Override
public List<String> fileContentTypes() {
return fileContentTypes;
}
};

@Override
public boolean deleteUploadedFilesOnEnd() {
return deleteUploadedFilesOnEnd;
Expand All @@ -27,6 +35,11 @@ public String uploadsDirectory() {
public Charset defaultCharset() {
return defaultCharset;
}

@Override
public MultiPart multiPart() {
return multiPart;
}
};
limits = new Limits() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.nio.charset.Charset;
import java.time.Duration;
import java.util.List;
import java.util.Optional;

public interface RuntimeConfiguration {
Expand All @@ -19,6 +20,12 @@ interface Body {
String uploadsDirectory();

Charset defaultCharset();

MultiPart multiPart();

interface MultiPart {
List<String> fileContentTypes();
}
}

interface Limits {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public boolean isBlockingAllowed() {
static Vertx vertx;
static ExecutorService executor;
boolean deleteUploadedFilesOnEnd = true;
List<String> fileContentTypes;
Path uploadPath;

private List<Consumer<ResteasyReactiveDeploymentManager.ScanStep>> scanCustomizers = new ArrayList<>();
Expand Down Expand Up @@ -149,6 +150,11 @@ public ResteasyReactiveUnitTest setDeleteUploadedFilesOnEnd(boolean deleteUpload
return this;
}

public ResteasyReactiveUnitTest setFileContentTypes(List<String> fileContentTypes) {
this.fileContentTypes = fileContentTypes;
return this;
}

public ResteasyReactiveUnitTest setUploadPath(Path uploadPath) {
this.uploadPath = uploadPath;
return this;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit f06081f

Please sign in to comment.