Skip to content

Commit

Permalink
Allow customization of default content type for Multipart handling
Browse files Browse the repository at this point in the history
Also changes the default to UTF-8 and apply some minor polish

Resolves: quarkusio#19527
  • Loading branch information
geoand committed Aug 23, 2021
1 parent 504e824 commit 816752b
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveInitialiser;
import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRecorder;
import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRuntimeRecorder;
import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveServerRuntimeConfig;
import io.quarkus.resteasy.reactive.server.runtime.ServerVertxAsyncFileMessageBodyWriter;
import io.quarkus.resteasy.reactive.server.runtime.ServerVertxBufferMessageBodyWriter;
import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.AuthenticationCompletionExceptionMapper;
Expand Down Expand Up @@ -599,11 +600,11 @@ private <T> T getEffectivePropertyValue(String legacyPropertyName, T newProperty
@Record(ExecutionTime.RUNTIME_INIT)
public void applyRuntimeConfig(ResteasyReactiveRuntimeRecorder recorder,
Optional<ResteasyReactiveDeploymentBuildItem> deployment,
HttpConfiguration httpConfiguration) {
HttpConfiguration httpConf, ResteasyReactiveServerRuntimeConfig resteasyReactiveServerRuntimeConf) {
if (!deployment.isPresent()) {
return;
}
recorder.configure(deployment.get().getDeployment(), httpConfiguration);
recorder.configure(deployment.get().getDeployment(), httpConf, resteasyReactiveServerRuntimeConf);
}

@BuildStep
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.quarkus.resteasy.reactive.server.test.multipart;

import static org.hamcrest.CoreMatchers.not;

import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;

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.resteasy.reactive.RestForm;
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.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.restassured.builder.MultiPartSpecBuilder;
import io.restassured.specification.MultiPartSpecification;

public class InvalidEncodingTest {

private static final String TEXT_WITH_ACCENTED_CHARACTERS = "Text with UTF-8 accented characters: é à è";

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(FeedbackBody.class, FeedbackResource.class)
.addAsResource(new StringAsset(
"quarkus.resteasy-reactive.multipart.input-part.default-charset=us-ascii"),
"application.properties"));

@Test
public void testMultipartEncoding() throws URISyntaxException {
MultiPartSpecification multiPartSpecification = new MultiPartSpecBuilder(TEXT_WITH_ACCENTED_CHARACTERS)
.controlName("content")
// we need to force the content-type to avoid having the charset included
// as we are testing the default behavior when no charset is defined
.header("Content-Type", "text/plain")
.charset(StandardCharsets.UTF_8)
.build();

RestAssured
.given()
.multiPart(multiPartSpecification)
.post("/test/multipart-encoding")
.then()
.statusCode(200)
.body(not(TEXT_WITH_ACCENTED_CHARACTERS));
}

@Path("/test")
public static class FeedbackResource {

@POST
@Path("/multipart-encoding")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA + ";charset=UTF-8")
public String postForm(@MultipartForm final FeedbackBody feedback) {
return feedback.content;
}
}

public static class FeedbackBody {
@RestForm("content")
public String content;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@
</capabilities>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.resteasy.reactive.server.runtime;

import java.nio.charset.Charset;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
Expand All @@ -15,27 +16,33 @@
@Recorder
public class ResteasyReactiveRuntimeRecorder {

public void configure(RuntimeValue<Deployment> deployment, HttpConfiguration configuration) {
public void configure(RuntimeValue<Deployment> deployment, HttpConfiguration httpConf,
ResteasyReactiveServerRuntimeConfig runtimeConf) {
List<RuntimeConfigurableServerRestHandler> runtimeConfigurableServerRestHandlers = deployment.getValue()
.getRuntimeConfigurableServerRestHandlers();
for (RuntimeConfigurableServerRestHandler handler : runtimeConfigurableServerRestHandlers) {
handler.configure(new RuntimeConfiguration() {
@Override
public Duration readTimeout() {
return configuration.readTimeout;
return httpConf.readTimeout;
}

@Override
public Body body() {
return new Body() {
@Override
public boolean deleteUploadedFilesOnEnd() {
return configuration.body.deleteUploadedFilesOnEnd;
return httpConf.body.deleteUploadedFilesOnEnd;
}

@Override
public String uploadsDirectory() {
return configuration.body.uploadsDirectory;
return httpConf.body.uploadsDirectory;
}

@Override
public Charset defaultCharset() {
return runtimeConf.multipart.inputPart.defaultCharset;
}
};
}
Expand All @@ -45,16 +52,16 @@ public Limits limits() {
return new Limits() {
@Override
public Optional<Long> maxBodySize() {
if (configuration.limits.maxBodySize.isPresent()) {
return Optional.of(configuration.limits.maxBodySize.get().asLongValue());
if (httpConf.limits.maxBodySize.isPresent()) {
return Optional.of(httpConf.limits.maxBodySize.get().asLongValue());
} else {
return Optional.empty();
}
}

@Override
public long maxFormAttributeSize() {
return configuration.limits.maxFormAttributeSize.asLongValue();
return httpConf.limits.maxFormAttributeSize.asLongValue();
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.resteasy.reactive.server.runtime;

import java.nio.charset.Charset;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(name = "resteasy-reactive", phase = ConfigPhase.RUN_TIME)
public class ResteasyReactiveServerRuntimeConfig {

/**
* Input part configuration.
*/
@ConfigItem
public MultipartConfigGroup multipart;

@ConfigGroup
public static class MultipartConfigGroup {

/**
* Input part configuration.
*/
@ConfigItem
public InputPartConfigGroup inputPart;
}

@ConfigGroup
public static class InputPartConfigGroup {

/**
* Default charset.
*/
@ConfigItem(defaultValue = "UTF-8")
public Charset defaultCharset;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf
boolean validConsumes = false;
if (consumes != null) {
for (String c : consumes) {
if (c.equals(MediaType.MULTIPART_FORM_DATA)) {
if (c.startsWith(MediaType.MULTIPART_FORM_DATA)) {
validConsumes = true;
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
Expand All @@ -25,7 +26,7 @@ public class FormEncodedDataDefinition implements FormParserFactory.ParserDefini
private static final Logger log = Logger.getLogger(FormEncodedDataDefinition.class);

public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
private String defaultEncoding = "ISO-8859-1";
private String defaultCharset = StandardCharsets.UTF_8.displayName();;
private boolean forceCreation = false; //if the parser should be created even if the correct headers are missing
private int maxParams = 1000;
private long maxAttributeSize = 2048;
Expand All @@ -38,7 +39,7 @@ public FormDataParser create(final ResteasyReactiveRequestContext exchange) {
String mimeType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE);
if (forceCreation || (mimeType != null && mimeType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED))) {

String charset = defaultEncoding;
String charset = defaultCharset;
String contentType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE);
if (contentType != null) {
String cs = HeaderUtil.extractQuotedValueFromHeader(contentType, "charset");
Expand All @@ -52,8 +53,8 @@ public FormDataParser create(final ResteasyReactiveRequestContext exchange) {
return null;
}

public String getDefaultEncoding() {
return defaultEncoding;
public String getDefaultCharset() {
return defaultCharset;
}

public boolean isForceCreation() {
Expand Down Expand Up @@ -83,8 +84,8 @@ public FormEncodedDataDefinition setForceCreation(boolean forceCreation) {
return this;
}

public FormEncodedDataDefinition setDefaultEncoding(final String defaultEncoding) {
this.defaultEncoding = defaultEncoding;
public FormEncodedDataDefinition setDefaultCharset(final String defaultCharset) {
this.defaultCharset = defaultCharset;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public interface ParserDefinition<T> {

FormDataParser create(final ResteasyReactiveRequestContext exchange);

T setDefaultEncoding(String charset);
T setDefaultCharset(String charset);
}

public static Builder builder(Supplier<Executor> executorSupplier) {
Expand Down Expand Up @@ -114,7 +114,7 @@ public Builder withDefaultCharset(String defaultCharset) {
public FormParserFactory build() {
if (defaultCharset != null) {
for (ParserDefinition parser : parsers) {
parser.setDefaultEncoding(defaultCharset);
parser.setDefaultCharset(defaultCharset);
}
}
return new FormParserFactory(parsers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,14 @@ public class MultiPartParserDefinition implements FormParserFactory.ParserDefini

private Path tempFileLocation;

private String defaultEncoding = StandardCharsets.ISO_8859_1.displayName();
private String defaultCharset = StandardCharsets.UTF_8.displayName();

private boolean deleteUploadsOnEnd = true;

private long maxIndividualFileSize = -1;

private long fileSizeThreshold;

private int maxParameters = 1000;
private long maxAttributeSize = 2048;
private long maxEntitySize = -1;

Expand All @@ -75,7 +74,7 @@ public FormDataParser create(final ResteasyReactiveRequestContext exchange) {
return null;
}
final MultiPartUploadHandler parser = new MultiPartUploadHandler(exchange, boundary, maxIndividualFileSize,
fileSizeThreshold, defaultEncoding, mimeType, maxAttributeSize, maxEntitySize);
fileSizeThreshold, defaultCharset, mimeType, maxAttributeSize, maxEntitySize);
exchange.registerCompletionCallback(new CompletionCallback() {
@Override
public void onComplete(Throwable throwable) {
Expand Down Expand Up @@ -119,12 +118,12 @@ public MultiPartParserDefinition setTempFileLocation(Path tempFileLocation) {
return this;
}

public String getDefaultEncoding() {
return defaultEncoding;
public String getDefaultCharset() {
return defaultCharset;
}

public MultiPartParserDefinition setDefaultEncoding(final String defaultEncoding) {
this.defaultEncoding = defaultEncoding;
public MultiPartParserDefinition setDefaultCharset(final String defaultCharset) {
this.defaultCharset = defaultCharset;
return this;
}

Expand Down Expand Up @@ -181,6 +180,7 @@ private MultiPartUploadHandler(final ResteasyReactiveRequestContext exchange, fi
this.fileSizeThreshold = fileSizeThreshold;
this.maxAttributeSize = maxAttributeSize;
this.maxEntitySize = maxEntitySize;
int maxParameters = 1000;
this.data = new FormData(maxParameters);
String charset = defaultEncoding;
if (contentType != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

public class FormBodyHandler implements ServerRestHandler, RuntimeConfigurableServerRestHandler {

private static final byte[] NO_BYTES = new byte[0];

private final boolean alsoSetInputStream;
private final Supplier<Executor> executorSupplier;
private volatile FormParserFactory formParserFactory;
Expand All @@ -43,9 +41,12 @@ public void configure(RuntimeConfiguration configuration) {
.setMaxAttributeSize(configuration.limits().maxFormAttributeSize())
.setMaxEntitySize(configuration.limits().maxBodySize().orElse(-1L))
.setDeleteUploadsOnEnd(configuration.body().deleteUploadedFilesOnEnd())
.setDefaultCharset(configuration.body().defaultCharset().name())
.setTempFileLocation(Path.of(configuration.body().uploadsDirectory())))

.addParser(new FormEncodedDataDefinition()
.setMaxAttributeSize(configuration.limits().maxFormAttributeSize()))
.setMaxAttributeSize(configuration.limits().maxFormAttributeSize())
.setDefaultCharset(configuration.body().defaultCharset().name()))
.build();

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jboss.resteasy.reactive.server.spi;

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

Expand All @@ -16,6 +17,8 @@ interface Body {
boolean deleteUploadedFilesOnEnd();

String uploadsDirectory();

Charset defaultCharset();
}

interface Limits {
Expand Down

0 comments on commit 816752b

Please sign in to comment.