diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 13577f5aff47b..65dbe81ffba0f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -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; @@ -599,11 +600,11 @@ private T getEffectivePropertyValue(String legacyPropertyName, T newProperty @Record(ExecutionTime.RUNTIME_INIT) public void applyRuntimeConfig(ResteasyReactiveRuntimeRecorder recorder, Optional 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 diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java new file mode 100644 index 0000000000000..8f7f829a0f0fe --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java @@ -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; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/pom.xml index 8c953594ebc94..a580781b3e548 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/pom.xml @@ -63,6 +63,18 @@ + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + 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 597dee682f820..54633188e5330 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 @@ -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; @@ -15,14 +16,15 @@ @Recorder public class ResteasyReactiveRuntimeRecorder { - public void configure(RuntimeValue deployment, HttpConfiguration configuration) { + public void configure(RuntimeValue deployment, HttpConfiguration httpConf, + ResteasyReactiveServerRuntimeConfig runtimeConf) { List runtimeConfigurableServerRestHandlers = deployment.getValue() .getRuntimeConfigurableServerRestHandlers(); for (RuntimeConfigurableServerRestHandler handler : runtimeConfigurableServerRestHandlers) { handler.configure(new RuntimeConfiguration() { @Override public Duration readTimeout() { - return configuration.readTimeout; + return httpConf.readTimeout; } @Override @@ -30,12 +32,17 @@ 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; } }; } @@ -45,8 +52,8 @@ public Limits limits() { return new Limits() { @Override public Optional 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(); } @@ -54,7 +61,7 @@ public Optional maxBodySize() { @Override public long maxFormAttributeSize() { - return configuration.limits.maxFormAttributeSize.asLongValue(); + return httpConf.limits.maxFormAttributeSize.asLongValue(); } }; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveServerRuntimeConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveServerRuntimeConfig.java new file mode 100644 index 0000000000000..cff31220d4ad8 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveServerRuntimeConfig.java @@ -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; + } +} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index e1a8f4d6eb5bd..c09283af77e5b 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -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; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java index ab4369160abba..1e2ec790529e1 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java @@ -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; @@ -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; @@ -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"); @@ -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() { @@ -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; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java index 21324c6b8408e..c83442401d2b0 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java @@ -43,7 +43,7 @@ public interface ParserDefinition { FormDataParser create(final ResteasyReactiveRequestContext exchange); - T setDefaultEncoding(String charset); + T setDefaultCharset(String charset); } public static Builder builder(Supplier executorSupplier) { @@ -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); 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 ec1e4fa875cfe..907013bf7d53c 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 @@ -41,7 +41,7 @@ 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; @@ -49,7 +49,6 @@ public class MultiPartParserDefinition implements FormParserFactory.ParserDefini private long fileSizeThreshold; - private int maxParameters = 1000; private long maxAttributeSize = 2048; private long maxEntitySize = -1; @@ -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) { @@ -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; } @@ -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) { 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 5d6fcbf4b23eb..1478096c4238d 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 @@ -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 executorSupplier; private volatile FormParserFactory formParserFactory; @@ -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 { 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 a60b187bc70d1..5bdd06534a537 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 @@ -1,5 +1,6 @@ package org.jboss.resteasy.reactive.server.spi; +import java.nio.charset.Charset; import java.time.Duration; import java.util.Optional; @@ -16,6 +17,8 @@ interface Body { boolean deleteUploadedFilesOnEnd(); String uploadsDirectory(); + + Charset defaultCharset(); } interface Limits {