From 983d4b3fcdea418b48010017d17573818d398553 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 4 Jun 2021 12:45:19 +1000 Subject: [PATCH] Do multipart parsing inside RR This gives us a lot more flexibility, and allows it to work with multiple backends. Fixes #17430 --- .../runtime/ServletRequestContext.java | 27 -- .../MultipartPopulatorGenerator.java | 23 +- .../deployment/ResteasyReactiveProcessor.java | 6 +- .../server/runtime/MultipartFormHandler.java | 279 ------------ ...QuarkusResteasyReactiveRequestContext.java | 5 +- .../runtime/ResteasyReactiveRecorder.java | 16 +- .../ResteasyReactiveRuntimeRecorder.java | 5 + .../runtime/multipart/MultipartSupport.java | 52 ++- .../runtime/multipart/QuarkusFileUpload.java | 45 -- .../reactive/common/headers/HeaderUtil.java | 108 +++++ .../reactive/common/util/URLUtils.java | 1 + .../core/ResteasyReactiveRequestContext.java | 43 +- .../core/multipart/DefaultFileUpload.java | 56 +++ .../server/core/multipart/FormData.java | 370 +++++++++++++++ .../server/core/multipart/FormDataParser.java | 50 ++ .../multipart/FormEncodedDataDefinition.java | 282 ++++++++++++ .../core/multipart/FormParserFactory.java | 125 +++++ .../multipart/MultiPartParserDefinition.java | 430 ++++++++++++++++++ .../core/multipart/MultipartParser.java | 415 +++++++++++++++++ .../startup/CustomServerRestHandlers.java | 8 +- .../startup/RuntimeResourceDeployment.java | 13 +- .../server/handlers/FormBodyHandler.java | 121 +++-- .../server/spi/RuntimeConfiguration.java | 2 + .../server/spi/ServerHttpRequest.java | 6 - .../vertx/VertxRequestContextFactory.java | 2 +- .../VertxResteasyReactiveRequestContext.java | 26 +- 26 files changed, 2038 insertions(+), 478 deletions(-) delete mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/MultipartFormHandler.java delete mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/QuarkusFileUpload.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/DefaultFileUpload.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormData.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormDataParser.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartParser.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java index 5873226f0ae6f..cfd0f04f77e51 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java @@ -6,7 +6,6 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; @@ -227,26 +226,6 @@ public void closeConnection() { context.response().close(); } - @Override - public String getFormAttribute(String name) { - if (context.queryParams().contains(name)) { - return null; - } - return request.getParameter(name); - } - - @Override - public List getAllFormAttributes(String name) { - if (context.queryParams().contains(name)) { - return Collections.emptyList(); - } - String[] values = request.getParameterValues(name); - if (values == null) { - return Collections.emptyList(); - } - return Arrays.asList(values); - } - @Override public String getQueryParam(String name) { if (!context.queryParams().contains(name)) { @@ -275,12 +254,6 @@ public boolean isRequestEnded() { return context.request().isEnded(); } - @Override - public void setExpectMultipart(boolean expectMultipart) { - //read the form data - request.getParameterMap(); - } - @Override public InputStream createInputStream(ByteBuffer existingData) { return new ServletResteasyReactiveInputStream(existingData, request); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java index a41931d2489c8..f05f9dd80b16b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java @@ -25,6 +25,7 @@ import org.jboss.resteasy.reactive.common.util.DeploymentUtils; import org.jboss.resteasy.reactive.common.util.types.TypeSignatureParser; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.DefaultFileUpload; import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext; import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; @@ -39,7 +40,6 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.resteasy.reactive.server.runtime.multipart.MultipartSupport; -import io.quarkus.resteasy.reactive.server.runtime.multipart.QuarkusFileUpload; final class MultipartPopulatorGenerator { @@ -273,7 +273,7 @@ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, In // uploaded file are present in the RoutingContext and are extracted using MultipartSupport#getFileUpload ResultHandle fileUploadHandle = populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "getFileUpload", QuarkusFileUpload.class, + MethodDescriptor.ofMethod(MultipartSupport.class, "getFileUpload", DefaultFileUpload.class, String.class, ResteasyReactiveRequestContext.class), formAttrNameHandle, rrCtxHandle); if (fieldDotName.equals(DotNames.FIELD_UPLOAD_NAME)) { @@ -285,7 +285,7 @@ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, In fileUploadNullTrue.breakScope(); BytecodeCreator fileUploadFalse = fileUploadNullBranch.falseBranch(); ResultHandle pathHandle = fileUploadFalse.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusFileUpload.class, "uploadedFile", Path.class), + MethodDescriptor.ofMethod(DefaultFileUpload.class, "uploadedFile", Path.class), fileUploadHandle); if (fieldDotName.equals(DotNames.PATH_NAME)) { fileUploadFalse.assign(resultHandle, pathHandle); @@ -348,9 +348,10 @@ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, In // in this case all we need to do is read the value of the form attribute populate.assign(resultHandle, - populate.invokeInterfaceMethod(MethodDescriptor.ofMethod(ServerHttpRequest.class, - "getFormAttribute", String.class, String.class), serverReqHandle, - formAttrNameHandle)); + populate.invokeVirtualMethod(MethodDescriptor.ofMethod(ResteasyReactiveRequestContext.class, + "getFormParameter", Object.class, String.class, boolean.class, boolean.class), + rrCtxHandle, + formAttrNameHandle, populate.load(true), populate.load(false))); } else { // we need to use the field type and the media type to locate a MessageBodyReader @@ -382,11 +383,11 @@ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, In MethodDescriptor.ofMethod(MediaType.class, "valueOf", MediaType.class, String.class), clinit.load(partType))); - ResultHandle formStrValueHandle = populate.invokeInterfaceMethod( - MethodDescriptor.ofMethod(ServerHttpRequest.class, - "getFormAttribute", String.class, String.class), - serverReqHandle, - formAttrNameHandle); + ResultHandle formStrValueHandle = populate.invokeVirtualMethod( + MethodDescriptor.ofMethod(ResteasyReactiveRequestContext.class, + "getFormParameter", Object.class, String.class, boolean.class, boolean.class), + rrCtxHandle, + formAttrNameHandle, populate.load(true), populate.load(false)); populate.assign(resultHandle, populate.invokeStaticMethod( MethodDescriptor.ofMethod(MultipartSupport.class, "convertFormAttribute", Object.class, 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 2657ba4d83311..9b893a9faaf08 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 @@ -82,6 +82,7 @@ import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; @@ -274,7 +275,8 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem ParamConverterProvidersBuildItem paramConverterProvidersBuildItem, ContextResolversBuildItem contextResolversBuildItem, List applicationClassPredicateBuildItems, - List methodScanners, ResteasyReactiveServerConfig serverConfig) + List methodScanners, ResteasyReactiveServerConfig serverConfig, + LaunchModeBuildItem launchModeBuildItem) throws NoSuchMethodException { if (!resourceScanningResultBuildItem.isPresent()) { @@ -549,7 +551,7 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an RuntimeValue deployment = recorder.createDeployment(deploymentInfo, beanContainerBuildItem.getValue(), shutdownContext, vertxConfig, requestContextFactoryBuildItem.map(RequestContextFactoryBuildItem::getFactory).orElse(null), - initClassFactory); + initClassFactory, launchModeBuildItem.getLaunchMode()); quarkusRestDeploymentBuildItemBuildProducer .produce(new ResteasyReactiveDeploymentBuildItem(deployment, deploymentPath)); 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 deleted file mode 100644 index fb19ed194ae30..0000000000000 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/MultipartFormHandler.java +++ /dev/null @@ -1,279 +0,0 @@ -package io.quarkus.resteasy.reactive.server.runtime; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.ws.rs.HttpMethod; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -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; -import io.vertx.core.http.HttpServerFileUpload; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.ext.web.FileUpload; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.impl.FileUploadImpl; - -/** - * ServerRestHandler implementation that handles the {@code multipart/form-data} media type. - * - * The code has been adapted from {@link io.vertx.ext.web.handler.impl.BodyHandlerImpl} - * and its main functionality is to populate {@code RoutingContext}'s fileUploads - */ -public class MultipartFormHandler implements RuntimeConfigurableServerRestHandler { - - private static final Logger LOG = Logger.getLogger(MultipartFormHandler.class); - - // in multipart requests, the body should not be available as a stream - private static final ByteArrayInputStream NO_BYTES_INPUT_STREAM = new ByteArrayInputStream(new byte[0]); - - private volatile String uploadsDirectory; - private volatile boolean deleteUploadedFilesOnEnd; - private volatile Optional maxBodySize; - private volatile ClassLoader tccl; - - @Override - public void configure(RuntimeConfiguration configuration) { - uploadsDirectory = configuration.body().uploadsDirectory(); - deleteUploadedFilesOnEnd = configuration.body().deleteUploadedFilesOnEnd(); - maxBodySize = configuration.limits().maxBodySize(); - // capture the proper TCCL in order to avoid losing it to Vert.x in dev-mode - tccl = Thread.currentThread().getContextClassLoader(); - - try { - Files.createDirectories(Paths.get(uploadsDirectory)); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public void handle(ResteasyReactiveRequestContext context) throws Exception { - // in some cases, with sub-resource locators or via request filters, - // it's possible we've already read the entity - if (context.hasInputStream()) { - // let's not set it twice - return; - } - if (context.serverRequest().getRequestMethod().equals(HttpMethod.GET) || - context.serverRequest().getRequestMethod().equals(HttpMethod.HEAD)) { - return; - } - HttpServerRequest httpServerRequest = context.serverRequest().unwrap(HttpServerRequest.class); - if (httpServerRequest.isEnded()) { - context.setInputStream(NO_BYTES_INPUT_STREAM); - } else { - httpServerRequest.setExpectMultipart(true); - httpServerRequest.pause(); - context.suspend(); - MultipartFormVertxHandler handler = new MultipartFormVertxHandler(context, tccl, uploadsDirectory, - deleteUploadedFilesOnEnd, maxBodySize); - httpServerRequest.handler(handler); - httpServerRequest.endHandler(new Handler() { - @Override - public void handle(Void event) { - handler.end(); - } - }); - httpServerRequest.resume(); - } - } - - private static class MultipartFormVertxHandler implements Handler { - private final ResteasyReactiveRequestContext rrContext; - private final RoutingContext context; - private final ClassLoader tccl; - - private final String uploadsDirectory; - private final boolean deleteUploadedFilesOnEnd; - private final Optional maxBodySize; - - boolean failed; - AtomicInteger uploadCount = new AtomicInteger(); - AtomicBoolean cleanup = new AtomicBoolean(false); - boolean ended; - long uploadSize = 0L; - - public MultipartFormVertxHandler(ResteasyReactiveRequestContext rrContext, ClassLoader tccl, String uploadsDirectory, - boolean deleteUploadedFilesOnEnd, Optional maxBodySize) { - this.rrContext = rrContext; - this.context = rrContext.serverRequest().unwrap(RoutingContext.class); - this.tccl = tccl; - this.uploadsDirectory = uploadsDirectory; - this.deleteUploadedFilesOnEnd = deleteUploadedFilesOnEnd; - this.maxBodySize = maxBodySize; - 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) { - if (maxBodySize.isPresent() && upload.isSizeAvailable()) { - // we can try to abort even before the upload starts - long size = uploadSize + upload.size(); - if (size > MultipartFormVertxHandler.this.maxBodySize.get()) { - failed = true; - restoreProperTCCL(); - rrContext.resume(new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE)); - return; - } - } - // we actually upload to a file with a generated filename - uploadCount.incrementAndGet(); - String uploadedFileName = new File(MultipartFormVertxHandler.this.uploadsDirectory, - UUID.randomUUID().toString()).getPath(); - upload.exceptionHandler(new UploadExceptionHandler(rrContext)); - upload.streamToFileSystem(uploadedFileName) - .onSuccess(new Handler() { - @Override - public void handle(Void x) { - uploadEnded(); - } - }) - .onFailure(new Handler() { - @Override - public void handle(Throwable ignored) { - new UploadExceptionHandler(rrContext); - } - }); - FileUploadImpl fileUpload = new FileUploadImpl(uploadedFileName, upload); - fileUploads.add(fileUpload); - } - }); - } - - private void cancelUploads() { - for (FileUpload fileUpload : context.fileUploads()) { - FileSystem fileSystem = context.vertx().fileSystem(); - try { - if (!fileUpload.cancel()) { - String uploadedFileName = fileUpload.uploadedFileName(); - fileSystem.delete(uploadedFileName, deleteResult -> { - if (deleteResult.failed()) { - LOG.warn("Delete of uploaded file failed: " + uploadedFileName, deleteResult.cause()); - } - }); - } - } catch (Exception e) { - LOG.debug("Unable to cancel file upload:", e); - } - } - } - - @Override - public void handle(Buffer buff) { - if (failed) { - return; - } - uploadSize += buff.length(); - if (maxBodySize.isPresent() && uploadSize > maxBodySize.get()) { - failed = true; - // enqueue a delete for the error uploads - context.vertx().runOnContext(new Handler() { - @Override - public void handle(Void v) { - MultipartFormVertxHandler.this.deleteFileUploads(); - } - }); - restoreProperTCCL(); - rrContext.resume(new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE)); - } - } - - private void restoreProperTCCL() { - Thread.currentThread().setContextClassLoader(tccl); - } - - void uploadEnded() { - int count = uploadCount.decrementAndGet(); - // only if parsing is done and count is 0 then all files have been processed - if (ended && count == 0) { - doEnd(); - } - } - - void end() { - // this marks the end of body parsing, calling doEnd should - // only be possible from this moment onwards - ended = true; - // only if parsing is done and count is 0 then all files have been processed - if (uploadCount.get() == 0) { - doEnd(); - } - } - - void doEnd() { - if (failed) { - deleteFileUploads(); - return; - } - if (deleteUploadedFilesOnEnd) { - context.addBodyEndHandler(x -> deleteFileUploads()); - } - rrContext.setInputStream(NO_BYTES_INPUT_STREAM); - restoreProperTCCL(); - rrContext.resume(); - } - - private void deleteFileUploads() { - if (cleanup.compareAndSet(false, true)) { - for (FileUpload fileUpload : context.fileUploads()) { - FileSystem fileSystem = context.vertx().fileSystem(); - String uploadedFileName = fileUpload.uploadedFileName(); - fileSystem.exists(uploadedFileName, existResult -> { - if (existResult.failed()) { - LOG.warn("Could not detect if uploaded file exists, not deleting: " + uploadedFileName, - existResult.cause()); - } else if (existResult.result()) { - fileSystem.delete(uploadedFileName, deleteResult -> { - if (deleteResult.failed()) { - LOG.warn("Delete of uploaded file failed: " + uploadedFileName, deleteResult.cause()); - } - }); - } - }); - } - } - } - - private class UploadExceptionHandler implements Handler { - private final ResteasyReactiveRequestContext rrContext; - - public UploadExceptionHandler(ResteasyReactiveRequestContext rrContext) { - this.rrContext = rrContext; - } - - @Override - public void handle(Throwable t) { - MultipartFormVertxHandler.this.deleteFileUploads(); - rrContext.resume(new WebApplicationException(t, Response.Status.INTERNAL_SERVER_ERROR)); - } - } - } -} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java index 0a5f7ccd5e279..378339b01c2f9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java @@ -21,8 +21,9 @@ public class QuarkusResteasyReactiveRequestContext extends VertxResteasyReactive public QuarkusResteasyReactiveRequestContext(Deployment deployment, ProvidersImpl providers, RoutingContext context, ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, - ServerRestHandler[] abortHandlerChain, CurrentIdentityAssociation currentIdentityAssociation) { - super(deployment, providers, context, requestContext, handlerChain, abortHandlerChain); + ServerRestHandler[] abortHandlerChain, ClassLoader devModeTccl, + CurrentIdentityAssociation currentIdentityAssociation) { + super(deployment, providers, context, requestContext, handlerChain, abortHandlerChain, devModeTccl); this.association = currentIdentityAssociation; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 08cfd617aea83..aba782b116f68 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -70,7 +70,8 @@ public RuntimeValue createDeployment(DeploymentInfo info, BeanContainer beanContainer, ShutdownContext shutdownContext, HttpBuildTimeConfig vertxConfig, RequestContextFactory contextFactory, - BeanFactory initClassFactory) { + BeanFactory initClassFactory, + LaunchMode launchMode) { CurrentRequestManager .setCurrentRequestInstance(new QuarkusCurrentRequest(beanContainer.instance(CurrentVertxRequest.class))); @@ -90,6 +91,7 @@ public void accept(Closeable closeable) { }; CurrentIdentityAssociation currentIdentityAssociation = Arc.container().instance(CurrentIdentityAssociation.class) .get(); + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); if (contextFactory == null) { contextFactory = new RequestContextFactory() { @Override @@ -99,14 +101,14 @@ public ResteasyReactiveRequestContext createContext(Deployment deployment, return new QuarkusResteasyReactiveRequestContext(deployment, providers, (RoutingContext) context, requestContext, handlerChain, - abortHandlerChain, currentIdentityAssociation); + abortHandlerChain, launchMode == LaunchMode.DEVELOPMENT ? tccl : null, currentIdentityAssociation); } }; } RuntimeDeploymentManager runtimeDeploymentManager = new RuntimeDeploymentManager(info, EXECUTOR_SUPPLIER, - new CustomServerRestHandlers(new BlockingInputHandlerSupplier(), new MultipartHandlerSupplier()), + new CustomServerRestHandlers(new BlockingInputHandlerSupplier()), closeTaskHandler, contextFactory, new ArcThreadSetupAction(beanContainer.requestContext()), vertxConfig.rootPath); Deployment deployment = runtimeDeploymentManager.deploy(); @@ -211,12 +213,4 @@ public ServerRestHandler get() { } } - private static class MultipartHandlerSupplier implements Supplier { - - @Override - public ServerRestHandler get() { - return new MultipartFormHandler(); - } - } - } 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 5febf37afd65e..597dee682f820 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 @@ -51,6 +51,11 @@ public Optional maxBodySize() { return Optional.empty(); } } + + @Override + public long maxFormAttributeSize() { + return configuration.limits.maxFormAttributeSize.asLongValue(); + } }; } }); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java index 5c27ade999682..6c3cd26afe430 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java @@ -9,8 +9,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Set; import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotSupportedException; @@ -21,12 +21,11 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.core.ServerSerialisers; +import org.jboss.resteasy.reactive.server.core.multipart.DefaultFileUpload; +import org.jboss.resteasy.reactive.server.core.multipart.FormData; import org.jboss.resteasy.reactive.server.handlers.RequestDeserializeHandler; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; -import io.vertx.ext.web.FileUpload; -import io.vertx.ext.web.RoutingContext; - /** * This class isn't used directly, it is however used by generated code meant to deal with multipart forms. */ @@ -87,21 +86,22 @@ public static Object convertFormAttribute(String value, Class type, Type generic throw new NotSupportedException("Media type '" + mediaType + "' in multipart request is not supported"); } - public static QuarkusFileUpload getFileUpload(String formName, ResteasyReactiveRequestContext context) { - List uploads = getFileUploads(formName, context); + public static DefaultFileUpload getFileUpload(String formName, ResteasyReactiveRequestContext context) { + List uploads = getFileUploads(formName, context); if (!uploads.isEmpty()) { return uploads.get(0); } return null; } - public static List getFileUploads(String formName, ResteasyReactiveRequestContext context) { - List result = new ArrayList<>(); - RoutingContext routingContext = context.unwrap(RoutingContext.class); - Set fileUploads = routingContext.fileUploads(); - for (FileUpload fileUpload : fileUploads) { - if (fileUpload.name().equals(formName)) { - result.add(new QuarkusFileUpload(fileUpload)); + public static List getFileUploads(String formName, ResteasyReactiveRequestContext context) { + List result = new ArrayList<>(); + FormData fileUploads = context.getFormData(); + if (fileUploads != null) { + for (FormData.FormValue fileUpload : fileUploads.get(formName)) { + if (fileUpload.isFileItem()) { + result.add(new DefaultFileUpload(formName, fileUpload)); + } } } return result; @@ -109,8 +109,8 @@ public static List getFileUploads(String formName, ResteasyRe public static List getJavaIOFileUploads(String formName, ResteasyReactiveRequestContext context) { List result = new ArrayList<>(); - List uploads = getFileUploads(formName, context); - for (QuarkusFileUpload upload : uploads) { + List uploads = getFileUploads(formName, context); + for (DefaultFileUpload upload : uploads) { result.add(upload.uploadedFile().toFile()); } return result; @@ -118,19 +118,25 @@ public static List getJavaIOFileUploads(String formName, ResteasyReactiveR public static List getJavaPathFileUploads(String formName, ResteasyReactiveRequestContext context) { List result = new ArrayList<>(); - List uploads = getFileUploads(formName, context); - for (QuarkusFileUpload upload : uploads) { + List uploads = getFileUploads(formName, context); + for (DefaultFileUpload upload : uploads) { result.add(upload.uploadedFile()); } return result; } - public static List getFileUploads(ResteasyReactiveRequestContext context) { - RoutingContext routingContext = context.unwrap(RoutingContext.class); - Set fileUploads = routingContext.fileUploads(); - List result = new ArrayList<>(fileUploads.size()); - for (FileUpload fileUpload : fileUploads) { - result.add(new QuarkusFileUpload(fileUpload)); + public static List getFileUploads(ResteasyReactiveRequestContext context) { + FormData formData = context.getFormData(); + if (formData == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (String name : formData) { + for (FormData.FormValue fileUpload : formData.get(name)) { + if (fileUpload.isFileItem()) { + result.add(new DefaultFileUpload(name, fileUpload)); + } + } } return result; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/QuarkusFileUpload.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/QuarkusFileUpload.java deleted file mode 100644 index 112b650989327..0000000000000 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/QuarkusFileUpload.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.quarkus.resteasy.reactive.server.runtime.multipart; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.jboss.resteasy.reactive.multipart.FileUpload; - -public class QuarkusFileUpload implements FileUpload { - - private final io.vertx.ext.web.FileUpload vertxFileUpload; - - public QuarkusFileUpload(io.vertx.ext.web.FileUpload vertxFileUpload) { - this.vertxFileUpload = vertxFileUpload; - } - - @Override - public String name() { - return vertxFileUpload.name(); - } - - @Override - public Path uploadedFile() { - return Paths.get(vertxFileUpload.uploadedFileName()); - } - - @Override - public String fileName() { - return vertxFileUpload.fileName(); - } - - @Override - public long size() { - return vertxFileUpload.size(); - } - - @Override - public String contentType() { - return vertxFileUpload.contentType(); - } - - @Override - public String charSet() { - return vertxFileUpload.charSet(); - } -} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/HeaderUtil.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/HeaderUtil.java index 2cd8d6925d2b7..806d22602cdca 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/HeaderUtil.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/HeaderUtil.java @@ -1,6 +1,8 @@ package org.jboss.resteasy.reactive.common.headers; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -305,4 +307,110 @@ public static List getAcceptableLanguages(MultivaluedMap + * content-disposition=form-data; name="my field" + * and the key is name then "my field" will be returned without the quotes. + * + * + * @param header The header + * @param key The key that identifies the token to extract + * @return The token, or null if it was not found + */ + public static String extractQuotedValueFromHeader(final String header, final String key) { + + int keypos = 0; + int pos = -1; + boolean whiteSpace = true; + boolean inQuotes = false; + for (int i = 0; i < header.length() - 1; ++i) { //-1 because we need room for the = at the end + //TODO: a more efficient matching algorithm + char c = header.charAt(i); + if (inQuotes) { + if (c == '"') { + inQuotes = false; + } + } else { + if (key.charAt(keypos) == c && (whiteSpace || keypos > 0)) { + keypos++; + whiteSpace = false; + } else if (c == '"') { + keypos = 0; + inQuotes = true; + whiteSpace = false; + } else { + keypos = 0; + whiteSpace = c == ' ' || c == ';' || c == '\t'; + } + if (keypos == key.length()) { + if (header.charAt(i + 1) == '=') { + pos = i + 2; + break; + } else { + keypos = 0; + } + } + } + + } + if (pos == -1) { + return null; + } + + int end; + int start = pos; + if (header.charAt(start) == '"') { + start++; + for (end = start; end < header.length(); ++end) { + char c = header.charAt(end); + if (c == '"') { + break; + } + } + return header.substring(start, end); + + } else { + //no quotes + for (end = start; end < header.length(); ++end) { + char c = header.charAt(end); + if (c == ' ' || c == '\t' || c == ';') { + break; + } + } + return header.substring(start, end); + } + } + + /** + * Extracts a quoted value from a header that has a given key. For instance if the header is + *

+ * content-disposition=form-data; filename*="utf-8''test.txt" + * and the key is filename* then "test.txt" will be returned after extracting character set and language + * (following RFC 2231) and performing URL decoding to the value using the specified encoding + * + * @param header The header + * @param key The key that identifies the token to extract + * @return The token, or null if it was not found + */ + public static String extractQuotedValueFromHeaderWithEncoding(final String header, final String key) { + String value = extractQuotedValueFromHeader(header, key); + if (value != null) { + return value; + } + value = extractQuotedValueFromHeader(header, key + "*"); + if (value != null) { + int characterSetDelimiter = value.indexOf('\''); + int languageDelimiter = value.lastIndexOf('\'', characterSetDelimiter + 1); + String characterSet = value.substring(0, characterSetDelimiter); + try { + String fileNameURLEncoded = value.substring(languageDelimiter + 1); + return URLDecoder.decode(fileNameURLEncoded, characterSet); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + return null; + } } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java index 14206c9b35c03..147ed710da741 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java @@ -126,6 +126,7 @@ public static String decode(String s, Charset enc, boolean decodeSlash, boolean buffer = new StringBuilder(); } buffer.append(s, 0, i); + needToChange = true; } /* * Starting with this instance of a character diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java index dd8d3106ac44d..1795139e5ee71 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java @@ -10,6 +10,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executor; @@ -27,6 +28,7 @@ import org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext; import org.jboss.resteasy.reactive.common.util.Encode; import org.jboss.resteasy.reactive.common.util.PathSegmentImpl; +import org.jboss.resteasy.reactive.server.core.multipart.FormData; import org.jboss.resteasy.reactive.server.core.serialization.EntityWriter; import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext; import org.jboss.resteasy.reactive.server.jaxrs.AsyncResponseImpl; @@ -138,6 +140,7 @@ public abstract class ResteasyReactiveRequestContext private SecurityContext securityContext; private OutputStream outputStream; private OutputStream underlyingOutputStream; + private FormData formData; public ResteasyReactiveRequestContext(Deployment deployment, ProvidersImpl providers, ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain) { @@ -841,20 +844,29 @@ public String getCookieParameter(String name) { @Override public Object getFormParameter(String name, boolean single, boolean encoded) { + if (formData == null) { + return null; + } if (single) { - String val = serverRequest().getFormAttribute(name); - if (encoded && val != null) { - val = Encode.encodeQueryParam(val); + FormData.FormValue val = formData.getFirst(name); + if (val == null || val.isFileItem()) { + return null; } - return val; - } - List strings = serverRequest().getAllFormAttributes(name); - if (encoded) { - List newStrings = new ArrayList<>(); - for (String i : strings) { - newStrings.add(Encode.encodeQueryParam(i)); + if (encoded) { + return Encode.encodeQueryParam(val.getValue()); + } + return val.getValue(); + } + Deque val = formData.get(name); + List strings = new ArrayList<>(); + if (val != null) { + for (FormData.FormValue i : val) { + if (encoded) { + strings.add(Encode.encodeQueryParam(i.getValue())); + } else { + strings.add(i.getValue()); + } } - return newStrings; } return strings; @@ -932,6 +944,15 @@ public String getResourceLocatorPathParam(String name) { return getResourceLocatorPathParam(name, previousResource); } + public FormData getFormData() { + return formData; + } + + public ResteasyReactiveRequestContext setFormData(FormData formData) { + this.formData = formData; + return this; + } + private String getResourceLocatorPathParam(String name, PreviousResource previousResource) { if (previousResource == null) { return null; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/DefaultFileUpload.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/DefaultFileUpload.java new file mode 100644 index 0000000000000..35c132c4cb08c --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/DefaultFileUpload.java @@ -0,0 +1,56 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.io.IOException; +import java.nio.file.Path; +import javax.ws.rs.core.HttpHeaders; +import org.jboss.resteasy.reactive.common.headers.HeaderUtil; +import org.jboss.resteasy.reactive.multipart.FileUpload; + +public class DefaultFileUpload implements FileUpload { + + private final String name; + private final FormData.FormValue fileUpload; + + public DefaultFileUpload(String name, FormData.FormValue fileUpload) { + this.name = name; + this.fileUpload = fileUpload; + } + + @Override + public String name() { + return name; + } + + @Override + public Path uploadedFile() { + return fileUpload.getFileItem().getFile(); + } + + @Override + public String fileName() { + return fileUpload.getFileName(); + } + + @Override + public long size() { + try { + return fileUpload.getFileItem().getFileSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String contentType() { + return fileUpload.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + } + + @Override + public String charSet() { + String ct = fileUpload.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + if (ct == null) { + return null; + } + return HeaderUtil.extractQuotedValueFromHeader(ct, "charset"); + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormData.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormData.java new file mode 100644 index 0000000000000..6c8933df5bb02 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormData.java @@ -0,0 +1,370 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap; + +/** + * Representation of form data. + */ +public final class FormData implements Iterable { + + private static final Logger log = Logger.getLogger(FormData.class); + + private final Map> values = new LinkedHashMap<>(); + + private final int maxValues; + private int valueCount = 0; + + public FormData(final int maxValues) { + this.maxValues = maxValues; + } + + public Iterator iterator() { + return values.keySet().iterator(); + } + + public FormValue getFirst(String name) { + final Deque deque = values.get(name); + return deque == null ? null : deque.peekFirst(); + } + + public FormValue getLast(String name) { + final Deque deque = values.get(name); + return deque == null ? null : deque.peekLast(); + } + + public Deque get(String name) { + return values.get(name); + } + + public void add(String name, byte[] value, String fileName, CaseInsensitiveMap headers) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, values = new ArrayDeque<>(1)); + } + values.add(new FormValueImpl(value, fileName, headers)); + if (++valueCount > maxValues) { + throw new RuntimeException("Param limit of " + maxValues + " was exceeded"); + } + } + + public void add(String name, String value) { + add(name, value, null, null); + } + + public void add(String name, String value, final CaseInsensitiveMap headers) { + add(name, value, null, headers); + } + + public void add(String name, String value, String charset, final CaseInsensitiveMap headers) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, values = new ArrayDeque<>(1)); + } + values.add(new FormValueImpl(value, charset, headers)); + if (++valueCount > maxValues) { + throw new RuntimeException("Param limit of " + maxValues + " was exceeded"); + } + } + + public void add(String name, Path value, String fileName, final CaseInsensitiveMap headers) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, values = new ArrayDeque<>(1)); + } + values.add(new FormValueImpl(value, fileName, headers)); + if (values.size() > maxValues) { + throw new RuntimeException("Param limit of " + maxValues + " was exceeded"); + } + if (++valueCount > maxValues) { + throw new RuntimeException("Param limit of " + maxValues + " was exceeded"); + } + } + + public void put(String name, String value, final CaseInsensitiveMap headers) { + Deque values = new ArrayDeque<>(1); + Deque old = this.values.put(name, values); + if (old != null) { + valueCount -= old.size(); + } + values.add(new FormValueImpl(value, headers)); + + if (++valueCount > maxValues) { + throw new RuntimeException("Param limit of " + maxValues + " was exceeded"); + } + } + + public Deque remove(String name) { + Deque old = values.remove(name); + if (old != null) { + valueCount -= old.size(); + } + return old; + } + + public boolean contains(String name) { + final Deque value = values.get(name); + return value != null && !value.isEmpty(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final FormData strings = (FormData) o; + + if (values != null ? !values.equals(strings.values) : strings.values != null) + return false; + + return true; + } + + @Override + public int hashCode() { + return values != null ? values.hashCode() : 0; + } + + @Override + public String toString() { + return "FormData{" + + "values=" + values + + '}'; + } + + public void deleteFiles() { + for (Deque i : values.values()) { + for (FormValue j : i) { + if (j.isFileItem() && !j.getFileItem().isInMemory()) { + try { + Files.deleteIfExists(j.getFileItem().file); + } catch (IOException e) { + log.error("Cannot remove uploaded file " + j.getFileItem().file, e); + } + } + } + } + } + + public interface FormValue { + + /** + * @return the simple string value. + * @throws IllegalStateException If this is not a simple string value + */ + String getValue(); + + /** + * @return The charset of the simple string value + */ + String getCharset(); + + /** + * Returns true if this is a file and not a simple string + * + * @return + */ + @Deprecated + boolean isFile(); + + /** + * @return The temp file that the file data was saved to + * + * @throws IllegalStateException if this is not a file + */ + @Deprecated + Path getPath(); + + @Deprecated + File getFile(); + + FileItem getFileItem(); + + boolean isFileItem(); + + /** + * @return The filename specified in the disposition header. + */ + String getFileName(); + + /** + * @return The headers that were present in the multipart request, or null if this was not a multipart request + */ + CaseInsensitiveMap getHeaders(); + } + + public static class FileItem { + private final Path file; + private final byte[] content; + + public FileItem(Path file) { + this.file = file; + this.content = null; + } + + public FileItem(byte[] content) { + this.file = null; + this.content = content; + } + + public boolean isInMemory() { + return file == null; + } + + public Path getFile() { + return file; + } + + public long getFileSize() throws IOException { + if (isInMemory()) { + return content.length; + } else { + return Files.size(file); + } + } + + public InputStream getInputStream() throws IOException { + if (file != null) { + return new BufferedInputStream(Files.newInputStream(file)); + } else { + return new ByteArrayInputStream(content); + } + } + + public void delete() throws IOException { + if (file != null) { + try { + Files.delete(file); + } catch (NoSuchFileException e) { //already deleted + } + } + } + + public void write(Path target) throws IOException { + if (file != null) { + try { + Files.move(file, target); + return; + } catch (IOException e) { + // ignore and let the Files.copy, outside + // this if block, take over and attempt to copy it + } + } + try (InputStream is = getInputStream()) { + Files.copy(is, target); + } + } + } + + static class FormValueImpl implements FormValue { + + private final String value; + private final String fileName; + private final CaseInsensitiveMap headers; + private final FileItem fileItem; + private final String charset; + + FormValueImpl(String value, CaseInsensitiveMap headers) { + this.value = value; + this.headers = headers; + this.fileName = null; + this.fileItem = null; + this.charset = null; + } + + FormValueImpl(String value, String charset, CaseInsensitiveMap headers) { + this.value = value; + this.charset = charset; + this.headers = headers; + this.fileName = null; + this.fileItem = null; + } + + FormValueImpl(Path file, final String fileName, CaseInsensitiveMap headers) { + this.fileItem = new FileItem(file); + this.headers = headers; + this.fileName = fileName; + this.value = null; + this.charset = null; + } + + FormValueImpl(byte[] data, String fileName, CaseInsensitiveMap headers) { + this.fileItem = new FileItem(data); + this.fileName = fileName; + this.headers = headers; + this.value = null; + this.charset = null; + } + + @Override + public String getValue() { + if (value == null) { + throw new RuntimeException("Form value is a file"); + } + return value; + } + + @Override + public String getCharset() { + return charset; + } + + @Override + public boolean isFile() { + return fileItem != null && !fileItem.isInMemory(); + } + + @Override + public Path getPath() { + if (fileItem == null) { + throw new RuntimeException("Form value is a string"); + } + if (fileItem.isInMemory()) { + throw new RuntimeException("Form value is a memory file"); + } + return fileItem.getFile(); + } + + @Override + public File getFile() { + return getPath().toFile(); + } + + @Override + public FileItem getFileItem() { + if (fileItem == null) { + throw new RuntimeException("Form value is a string"); + } + return fileItem; + } + + @Override + public boolean isFileItem() { + return fileItem != null; + } + + @Override + public CaseInsensitiveMap getHeaders() { + return headers; + } + + public String getFileName() { + return fileName; + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormDataParser.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormDataParser.java new file mode 100644 index 0000000000000..0cc0bd6f82916 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormDataParser.java @@ -0,0 +1,50 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.io.Closeable; +import java.io.IOException; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; + +/** + * Parser for form data. This can be used by down-stream handlers to parse + * form data. + *

+ * This parser must be closed to make sure any temporary files have been cleaned up. + * + * @author Stuart Douglas + */ +public interface FormDataParser extends Closeable { + + /** + * Parse the form data asynchronously. If all the data cannot be read immediately then a read listener will be + * registered, and the data will be parsed by the read thread. + *

+ * The method can either invoke the next handler directly, or may delegate to the IO thread + * to perform the parsing. + */ + void parse() throws Exception; + + /** + * Parse the data, blocking the current thread until parsing is complete. For blocking handlers this method is + * more efficient than {@link #parse(ResteasyReactiveRequestContext next)}, as the calling thread should do that + * actual parsing, rather than the read thread + * + * @return The parsed form data + * @throws IOException If the data could not be read + */ + FormData parseBlocking() throws IOException; + + /** + * Closes the parser, and removes and temporary files that may have been created. + * + * @throws IOException + */ + void close() throws IOException; + + /** + * Sets the character encoding that will be used by this parser. If the request is already processed this will have + * no effect + * + * @param encoding The encoding + */ + void setCharacterEncoding(String encoding); +} 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 new file mode 100644 index 0000000000000..9741f69d21a88 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java @@ -0,0 +1,282 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.common.headers.HeaderUtil; +import org.jboss.resteasy.reactive.common.util.URLUtils; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; + +/** + * Parser definition for form encoded data. This handler takes effect for any request that has a mime type + * of application/x-www-form-urlencoded. The handler attaches a {@link FormDataParser} to the chain + * that can parse the underlying form data asynchronously. + * + * @author Stuart Douglas + */ +public class FormEncodedDataDefinition implements FormParserFactory.ParserDefinition { + + 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 boolean forceCreation = false; //if the parser should be created even if the correct headers are missing + private int maxParams = 1000; + private long maxAttributeSize = 2048; + + public FormEncodedDataDefinition() { + } + + @Override + 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 contentType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE); + if (contentType != null) { + String cs = HeaderUtil.extractQuotedValueFromHeader(contentType, "charset"); + if (cs != null) { + charset = cs; + } + } + log.tracef("Created form encoded parser for %s", exchange); + return new FormEncodedDataParser(charset, exchange, maxParams, maxAttributeSize); + } + return null; + } + + public String getDefaultEncoding() { + return defaultEncoding; + } + + public boolean isForceCreation() { + return forceCreation; + } + + public int getMaxParams() { + return maxParams; + } + + public long getMaxAttributeSize() { + return maxAttributeSize; + } + + public FormEncodedDataDefinition setMaxAttributeSize(long maxAttributeSize) { + this.maxAttributeSize = maxAttributeSize; + return this; + } + + public FormEncodedDataDefinition setMaxParams(int maxParams) { + this.maxParams = maxParams; + return this; + } + + public FormEncodedDataDefinition setForceCreation(boolean forceCreation) { + this.forceCreation = forceCreation; + return this; + } + + public FormEncodedDataDefinition setDefaultEncoding(final String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + return this; + } + + private static final class FormEncodedDataParser implements ServerHttpRequest.ReadCallback, FormDataParser { + + private final ResteasyReactiveRequestContext exchange; + private final FormData data; + private final StringBuilder builder = new StringBuilder(); + private final long maxAttributeSize; + private String name = null; + private String charset; + + //0= parsing name + //1=parsing name, decode required + //2=parsing value + //3=parsing value, decode required + //4=finished + private int state = 0; + + private FormEncodedDataParser(final String charset, final ResteasyReactiveRequestContext exchange, int maxParams, + long maxAttributeSize) { + this.exchange = exchange; + this.charset = charset; + this.data = new FormData(maxParams); + this.maxAttributeSize = maxAttributeSize; + } + + private void doParse(final ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + byte n = buffer.get(); + switch (state) { + case 0: { + if (n == '=') { + if (builder.length() > maxAttributeSize) { + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + name = builder.toString(); + builder.setLength(0); + state = 2; + } else if (n == '&') { + if (builder.length() > maxAttributeSize) { + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + addPair(builder.toString(), ""); + builder.setLength(0); + state = 0; + } else if (n == '%' || n == '+' || n < 0) { + state = 1; + builder.append((char) (n & 0xFF)); + } else { + builder.append((char) n); + } + break; + } + case 1: { + if (n == '=') { + if (builder.length() > maxAttributeSize) { + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + name = decodeParameterName(builder.toString(), charset, true, new StringBuilder()); + builder.setLength(0); + state = 2; + } else if (n == '&') { + addPair(decodeParameterName(builder.toString(), charset, true, new StringBuilder()), ""); + builder.setLength(0); + state = 0; + } else { + builder.append((char) (n & 0xFF)); + } + break; + } + case 2: { + if (n == '&') { + if (builder.length() > maxAttributeSize) { + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + addPair(name, builder.toString()); + builder.setLength(0); + state = 0; + } else if (n == '%' || n == '+' || n < 0) { + state = 3; + builder.append((char) (n & 0xFF)); + } else { + builder.append((char) n); + } + break; + } + case 3: { + if (n == '&') { + if (builder.length() > maxAttributeSize) { + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + addPair(name, decodeParameterValue(name, builder.toString(), charset, true, new StringBuilder())); + builder.setLength(0); + state = 0; + } else { + builder.append((char) (n & 0xFF)); + } + break; + } + } + } + if (builder.length() > maxAttributeSize) { + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + } + + private void addPair(String name, String value) { + //if there was exception during decoding ignore the parameter [UNDERTOW-1554] + if (name != null && value != null) { + data.add(name, value); + } + } + + private String decodeParameterValue(String name, String value, String charset, boolean decodeSlash, + StringBuilder stringBuilder) { + return URLUtils.decode(value, Charset.forName(charset), decodeSlash, stringBuilder); + } + + private String decodeParameterName(String name, String charset, boolean decodeSlash, StringBuilder stringBuilder) { + return URLUtils.decode(name, Charset.forName(charset), decodeSlash, stringBuilder); + } + + @Override + public void parse() throws Exception { + if (exchange.getFormData() != null) { + return; + } + exchange.suspend(); + exchange.serverRequest().setReadListener(this); + exchange.serverRequest().resumeRequestInput(); + } + + @Override + public FormData parseBlocking() throws IOException { + final FormData existing = exchange.getFormData(); + if (existing != null) { + return existing; + } + + try (InputStream input = exchange.getInputStream()) { + int c; + byte[] data = new byte[1024]; + while ((c = input.read(data)) > 0) { + ByteBuffer buf = ByteBuffer.wrap(data, 0, c); + doParse(buf); + } + inputDone(); + return this.data; + } + } + + @Override + public void close() throws IOException { + + } + + @Override + public void setCharacterEncoding(final String encoding) { + this.charset = encoding; + } + + @Override + public void done() { + inputDone(); + exchange.resume(); + } + + private void inputDone() { + if (state == 2) { + addPair(name, builder.toString()); + } else if (state == 3) { + addPair(name, decodeParameterValue(name, builder.toString(), charset, true, new StringBuilder())); + } else if (builder.length() > 0) { + if (state == 1) { + addPair(decodeParameterName(builder.toString(), charset, true, new StringBuilder()), ""); + } else { + addPair(builder.toString(), ""); + } + } + state = 4; + exchange.setFormData(data); + } + + @Override + public void data(ByteBuffer data) { + try { + doParse(data); + } catch (Exception e) { + exchange.resume(e); + } + } + } + +} 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 new file mode 100644 index 0000000000000..21324c6b8408e --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java @@ -0,0 +1,125 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; + +/** + * Factory class that can create a form data parser for a given request. + *

+ * It does this by iterating the available parser definitions, and returning + * the first parser that is created. + * + * @author Stuart Douglas + */ +public class FormParserFactory { + + private final ParserDefinition[] parserDefinitions; + + FormParserFactory(final List parserDefinitions) { + this.parserDefinitions = parserDefinitions.toArray(new ParserDefinition[parserDefinitions.size()]); + } + + /** + * Creates a form data parser for this request. + * + * @param exchange The exchange + * @return A form data parser, or null if there is no parser registered for the request content type + */ + public FormDataParser createParser(final ResteasyReactiveRequestContext exchange) { + for (int i = 0; i < parserDefinitions.length; ++i) { + FormDataParser parser = parserDefinitions[i].create(exchange); + if (parser != null) { + return parser; + } + } + return null; + } + + public interface ParserDefinition { + + FormDataParser create(final ResteasyReactiveRequestContext exchange); + + T setDefaultEncoding(String charset); + } + + public static Builder builder(Supplier executorSupplier) { + return builder(true, executorSupplier); + } + + public static Builder builder(boolean includeDefault, Supplier executorSupplier) { + Builder builder = new Builder(); + if (includeDefault) { + builder.addParsers(new FormEncodedDataDefinition(), new MultiPartParserDefinition(executorSupplier)); + } + return builder; + } + + public static class Builder { + + private List parsers = new ArrayList<>(); + + private String defaultCharset = null; + + public Builder addParser(final ParserDefinition definition) { + parsers.add(definition); + return this; + } + + public Builder addParsers(final ParserDefinition... definition) { + parsers.addAll(Arrays.asList(definition)); + return this; + } + + public Builder addParsers(final List definition) { + parsers.addAll(definition); + return this; + } + + public List getParsers() { + return parsers; + } + + public void setParsers(List parsers) { + this.parsers = parsers; + } + + /** + * A chainable version of {@link #setParsers}. + */ + public Builder withParsers(List parsers) { + setParsers(parsers); + return this; + } + + public String getDefaultCharset() { + return defaultCharset; + } + + public void setDefaultCharset(String defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * A chainable version of {@link #setDefaultCharset}. + */ + public Builder withDefaultCharset(String defaultCharset) { + setDefaultCharset(defaultCharset); + return this; + } + + public FormParserFactory build() { + if (defaultCharset != null) { + for (ParserDefinition parser : parsers) { + parser.setDefaultEncoding(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 new file mode 100644 index 0000000000000..2322786c18c00 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java @@ -0,0 +1,430 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.CompletionCallback; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.common.headers.HeaderUtil; +import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; + +/** + * @author Stuart Douglas + */ +public class MultiPartParserDefinition implements FormParserFactory.ParserDefinition { + + private static final Logger log = Logger.getLogger(MultiPartParserDefinition.class); + + public static final String MULTIPART_FORM_DATA = "multipart/form-data"; + + private final Supplier executorSupplier; + + private Path tempFileLocation; + + private String defaultEncoding = StandardCharsets.ISO_8859_1.displayName(); + + private boolean deleteUploadsOnEnd = true; + + private long maxIndividualFileSize = -1; + + private long fileSizeThreshold; + + private int maxParameters = 1000; + private long maxAttributeSize = 2048; + private long maxEntitySize = -1; + + public MultiPartParserDefinition(Supplier executorSupplier) { + this.executorSupplier = executorSupplier; + tempFileLocation = Paths.get(System.getProperty("java.io.tmpdir")); + } + + public MultiPartParserDefinition(Supplier executorSupplier, final Path tempDir) { + this.executorSupplier = executorSupplier; + tempFileLocation = tempDir; + } + + @Override + public FormDataParser create(final ResteasyReactiveRequestContext exchange) { + String mimeType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE); + if (mimeType != null && mimeType.startsWith(MULTIPART_FORM_DATA)) { + String boundary = HeaderUtil.extractQuotedValueFromHeader(mimeType, "boundary"); + if (boundary == null) { + log.debugf( + "Could not find boundary in multipart request with ContentType: %s, multipart data will not be available", + mimeType); + return null; + } + final MultiPartUploadHandler parser = new MultiPartUploadHandler(exchange, boundary, maxIndividualFileSize, + fileSizeThreshold, defaultEncoding, mimeType, maxAttributeSize, maxEntitySize); + exchange.registerCompletionCallback(new CompletionCallback() { + @Override + public void onComplete(Throwable throwable) { + try { + parser.close(); + } catch (IOException e) { + log.error("Failed to close multipart parser", e); + } + } + }); + return parser; + + } + return null; + } + + public long getMaxAttributeSize() { + return maxAttributeSize; + } + + public MultiPartParserDefinition setMaxAttributeSize(long maxAttributeSize) { + this.maxAttributeSize = maxAttributeSize; + return this; + } + + public boolean isDeleteUploadsOnEnd() { + return deleteUploadsOnEnd; + } + + public MultiPartParserDefinition setDeleteUploadsOnEnd(boolean deleteUploadsOnEnd) { + this.deleteUploadsOnEnd = deleteUploadsOnEnd; + return this; + } + + public Path getTempFileLocation() { + return tempFileLocation; + } + + public MultiPartParserDefinition setTempFileLocation(Path tempFileLocation) { + this.tempFileLocation = tempFileLocation; + return this; + } + + public String getDefaultEncoding() { + return defaultEncoding; + } + + public MultiPartParserDefinition setDefaultEncoding(final String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + return this; + } + + public long getMaxIndividualFileSize() { + return maxIndividualFileSize; + } + + public MultiPartParserDefinition setMaxIndividualFileSize(final long maxIndividualFileSize) { + this.maxIndividualFileSize = maxIndividualFileSize; + return this; + } + + public MultiPartParserDefinition setFileSizeThreshold(long fileSizeThreshold) { + this.fileSizeThreshold = fileSizeThreshold; + return this; + } + + public long getMaxEntitySize() { + return maxEntitySize; + } + + public MultiPartParserDefinition setMaxEntitySize(long maxEntitySize) { + this.maxEntitySize = maxEntitySize; + return this; + } + + private final class MultiPartUploadHandler implements FormDataParser, MultipartParser.PartHandler { + + private final ResteasyReactiveRequestContext exchange; + private final FormData data; + private final List createdFiles = new ArrayList<>(); + private final long maxIndividualFileSize; + private final long fileSizeThreshold; + private final long maxAttributeSize; + private final long maxEntitySize; + private String defaultEncoding; + + private final ByteArrayOutputStream contentBytes = new ByteArrayOutputStream(); + private String currentName; + private String fileName; + private Path file; + private FileChannel fileChannel; + private CaseInsensitiveMap headers; + private long currentFileSize; + private long currentEntitySize; + private final MultipartParser.ParseState parser; + + private MultiPartUploadHandler(final ResteasyReactiveRequestContext exchange, final String boundary, + final long maxIndividualFileSize, final long fileSizeThreshold, final String defaultEncoding, + String contentType, long maxAttributeSize, long maxEntitySize) { + this.exchange = exchange; + this.maxIndividualFileSize = maxIndividualFileSize; + this.defaultEncoding = defaultEncoding; + this.fileSizeThreshold = fileSizeThreshold; + this.maxAttributeSize = maxAttributeSize; + this.maxEntitySize = maxEntitySize; + this.data = new FormData(maxParameters); + String charset = defaultEncoding; + if (contentType != null) { + String value = HeaderUtil.extractQuotedValueFromHeader(contentType, "charset"); + if (value != null) { + charset = value; + } + } + this.parser = MultipartParser.beginParse(this, boundary.getBytes(StandardCharsets.US_ASCII), charset); + + } + + @Override + public void parse() throws Exception { + if (exchange.getFormData() != null) { + return; + } + //we need to delegate to a thread pool + //as we parse with blocking operations + exchange.suspend(); + exchange.serverRequest().setReadListener(new NonBlockingParseTask(executorSupplier.get())); + exchange.serverRequest().resumeRequestInput(); + } + + @Override + public FormData parseBlocking() throws IOException { + final FormData existing = exchange.getFormData(); + if (existing != null) { + return existing; + } + try (InputStream inputStream = exchange.getInputStream()) { + byte[] buf = new byte[1024]; + int c; + while ((c = inputStream.read(buf)) > 0) { + parser.parse(ByteBuffer.wrap(buf, 0, c)); + } + if (!parser.isComplete()) { + throw new IOException("Connection terminated parsing multipart request"); + } + exchange.setFormData(data); + } catch (RuntimeException e) { + throw new IOException(e); + } + return data; + } + + @Override + public void beginPart(final CaseInsensitiveMap headers) { + this.currentFileSize = 0; + this.headers = headers; + final String disposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION); + if (disposition != null) { + if (disposition.startsWith("form-data")) { + currentName = HeaderUtil.extractQuotedValueFromHeader(disposition, "name"); + fileName = HeaderUtil.extractQuotedValueFromHeaderWithEncoding(disposition, "filename"); + if (fileName != null && fileSizeThreshold == 0) { + try { + if (tempFileLocation != null) { + file = Files.createTempFile(tempFileLocation, "undertow", "upload"); + } else { + file = Files.createTempFile("undertow", "upload"); + } + createdFiles.add(file); + fileChannel = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + } + + @Override + public void data(final ByteBuffer buffer) throws IOException { + this.currentFileSize += buffer.remaining(); + this.currentEntitySize += buffer.remaining(); + if (maxEntitySize > 0 && currentEntitySize > maxEntitySize) { + data.deleteFiles(); + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + if (this.maxIndividualFileSize > 0 && this.currentFileSize > this.maxIndividualFileSize) { + data.deleteFiles(); + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + if (file == null && fileName != null && fileSizeThreshold < this.currentFileSize) { + try { + if (tempFileLocation != null) { + file = Files.createTempFile(tempFileLocation, "undertow", "upload"); + } else { + file = Files.createTempFile("undertow", "upload"); + } + createdFiles.add(file); + + FileOutputStream fileOutputStream = new FileOutputStream(file.toFile()); + contentBytes.writeTo(fileOutputStream); + + fileChannel = fileOutputStream.getChannel(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + if (file == null) { + while (buffer.hasRemaining()) { + contentBytes.write(buffer.get()); + } + if (maxAttributeSize > 0 && contentBytes.size() > maxAttributeSize) { + data.deleteFiles(); + throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE); + } + } else { + fileChannel.write(buffer); + } + } + + @Override + public void endPart() { + if (file != null) { + data.add(currentName, file, fileName, headers); + file = null; + contentBytes.reset(); + try { + fileChannel.close(); + fileChannel = null; + } catch (IOException e) { + throw new RuntimeException(e); + } + } else if (fileName != null) { + data.add(currentName, Arrays.copyOf(contentBytes.toByteArray(), contentBytes.size()), fileName, headers); + contentBytes.reset(); + } else { + + try { + String charset = defaultEncoding; + String contentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + if (contentType != null) { + String cs = HeaderUtil.extractQuotedValueFromHeader(contentType, "charset"); + if (cs != null) { + charset = cs; + } + } + + data.add(currentName, new String(contentBytes.toByteArray(), charset), charset, headers); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + contentBytes.reset(); + } + } + + public List getCreatedFiles() { + return createdFiles; + } + + @Override + public void close() throws IOException { + if (fileChannel != null) { + fileChannel.close(); + } + //we have to dispatch this, as it may result in file IO + if (deleteUploadsOnEnd) { + deleteFiles(); + } + } + + private void deleteFiles() { + final List files = new ArrayList<>(getCreatedFiles()); + executorSupplier.get().execute(new Runnable() { + @Override + public void run() { + for (final Path file : files) { + if (Files.exists(file)) { + try { + Files.delete(file); + } catch (NoSuchFileException e) { // ignore + } catch (IOException e) { + log.error("Cannot remove uploaded file " + file, e); + } + } + } + } + + }); + } + + @Override + public void setCharacterEncoding(final String encoding) { + this.defaultEncoding = encoding; + parser.setCharacterEncoding(encoding); + } + + private final class NonBlockingParseTask implements ServerHttpRequest.ReadCallback { + + private final Executor executor; + + private NonBlockingParseTask(Executor executor) { + this.executor = executor; + } + + @Override + public void done() { + if (parser.isComplete()) { + exchange.setFormData(data); + exchange.resume(); + } else { + exchange.resume(new IOException("Connection terminated reading multipart data")); + } + } + + @Override + public void data(ByteBuffer data) { + exchange.serverRequest().pauseRequestInput(); + executor.execute(new Runnable() { + @Override + public void run() { + try { + parser.parse(data); + exchange.serverRequest().resumeRequestInput(); + } catch (Throwable t) { + exchange.resume(t); + } + } + }); + } + } + } + + public static class FileTooLargeException extends IOException { + + public FileTooLargeException() { + super(); + } + + public FileTooLargeException(String message) { + super(message); + } + + public FileTooLargeException(String message, Throwable cause) { + super(message, cause); + } + + public FileTooLargeException(Throwable cause) { + super(cause); + } + } + +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartParser.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartParser.java new file mode 100644 index 0000000000000..76cfd1f46cd10 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartParser.java @@ -0,0 +1,415 @@ +package org.jboss.resteasy.reactive.server.core.multipart; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.Collections; +import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap; + +/** + * @author Stuart Douglas + */ +public class MultipartParser { + + /** + * The Horizontal Tab ASCII character value; + */ + public static final byte HTAB = 0x09; + + /** + * The Carriage Return ASCII character value. + */ + public static final byte CR = 0x0D; + + /** + * The Line Feed ASCII character value. + */ + public static final byte LF = 0x0A; + + /** + * The Space ASCII character value; + */ + public static final byte SP = 0x20; + + /** + * The dash (-) ASCII character value. + */ + public static final byte DASH = 0x2D; + + /** + * A byte sequence that precedes a boundary (CRLF--). + */ + private static final byte[] BOUNDARY_PREFIX = { CR, LF, DASH, DASH }; + public static final String CONTENT_TRANSFER_ENCODING = "content-transfer-encoding"; + + public interface PartHandler { + void beginPart(final CaseInsensitiveMap headers); + + void data(final ByteBuffer buffer) throws IOException; + + void endPart(); + } + + public static ParseState beginParse(final PartHandler handler, final byte[] boundary, final String requestCharset) { + + // We prepend CR/LF to the boundary to chop trailing CR/LF from + // body-data tokens. + byte[] boundaryToken = new byte[boundary.length + BOUNDARY_PREFIX.length]; + System.arraycopy(BOUNDARY_PREFIX, 0, boundaryToken, 0, BOUNDARY_PREFIX.length); + System.arraycopy(boundary, 0, boundaryToken, BOUNDARY_PREFIX.length, boundary.length); + return new ParseState(handler, requestCharset, boundaryToken); + } + + public static class ParseState { + private final PartHandler partHandler; + private String requestCharset; + /** + * The boundary, complete with the initial CRLF-- + */ + private final byte[] boundary; + + //0=preamble + private int state = 0; + private int subState = Integer.MAX_VALUE; // used for preamble parsing + private ByteArrayOutputStream currentString = null; + private String currentHeaderName = null; + private CaseInsensitiveMap headers; + private Encoding encodingHandler; + + public ParseState(final PartHandler partHandler, String requestCharset, final byte[] boundary) { + this.partHandler = partHandler; + this.requestCharset = requestCharset; + this.boundary = boundary; + } + + public void setCharacterEncoding(String encoding) { + requestCharset = encoding; + } + + public void parse(ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + switch (state) { + case 0: { + preamble(buffer); + break; + } + case 1: { + headerName(buffer); + break; + } + case 2: { + headerValue(buffer); + break; + } + case 3: { + entity(buffer); + break; + } + case -1: { + return; + } + default: { + throw new IllegalStateException("" + state); + } + } + } + } + + private void preamble(final ByteBuffer buffer) { + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (subState >= 0) { + //handle the case of no preamble. In this case there is no CRLF + if (subState == Integer.MAX_VALUE) { + if (boundary[2] == b) { + subState = 2; + } else { + subState = 0; + } + } + if (b == boundary[subState]) { + subState++; + if (subState == boundary.length) { + subState = -1; + } + } else if (b == boundary[0]) { + subState = 1; + } else { + subState = 0; + } + } else if (subState == -1) { + if (b == CR) { + subState = -2; + } + } else if (subState == -2) { + if (b == LF) { + subState = 0; + state = 1;//preamble is done + headers = new CaseInsensitiveMap(); + return; + } else { + subState = -1; + } + } + } + } + + private void headerName(final ByteBuffer buffer) throws MalformedMessageException, UnsupportedEncodingException { + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (b == ':') { + if (currentString == null || subState != 0) { + throw new MalformedMessageException(); + } else { + currentHeaderName = new String(currentString.toByteArray(), requestCharset); + currentString.reset(); + subState = 0; + state = 2; + return; + } + } else if (b == CR) { + if (currentString != null) { + throw new MalformedMessageException(); + } else { + subState = 1; + } + } else if (b == LF) { + if (currentString != null || subState != 1) { + throw new MalformedMessageException(); + } + state = 3; + subState = 0; + partHandler.beginPart(headers); + //select the appropriate encoding + String encoding = headers.getFirst(CONTENT_TRANSFER_ENCODING); + if (encoding == null) { + encodingHandler = new IdentityEncoding(); + } else if (encoding.equalsIgnoreCase("base64")) { + encodingHandler = new Base64Encoding(); + } else if (encoding.equalsIgnoreCase("quoted-printable")) { + encodingHandler = new QuotedPrintableEncoding(); + } else { + encodingHandler = new IdentityEncoding(); + } + headers = null; + return; + + } else { + if (subState != 0) { + throw new MalformedMessageException(); + } else if (currentString == null) { + currentString = new ByteArrayOutputStream(); + } + currentString.write(b); + } + } + } + + private void headerValue(final ByteBuffer buffer) throws MalformedMessageException, UnsupportedEncodingException { + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (subState == 2) { + if (b == CR) { //end of headers section + headers.put(currentHeaderName.trim(), + Collections.singletonList(new String(currentString.toByteArray(), requestCharset).trim())); + //set state for headerName to verify end of headers section + state = 1; + subState = 1; //CR already encountered + currentString = null; + return; + } else if (b == SP || b == HTAB) { //multi-line header + currentString.write(b); + subState = 0; + } else { //next header name + headers.put(currentHeaderName.trim(), + Collections.singletonList(new String(currentString.toByteArray(), requestCharset).trim())); + //set state for headerName to collect next header's name + state = 1; + subState = 0; + //start name collection for headerName to finish + currentString = new ByteArrayOutputStream(); + currentString.write(b); + return; + } + } else if (b == CR) { + subState = 1; + } else if (b == LF) { + if (subState != 1) { + throw new MalformedMessageException(); + } + subState = 2; + } else { + if (subState != 0) { + throw new MalformedMessageException(); + } + currentString.write(b); + } + } + } + + private void entity(final ByteBuffer buffer) throws IOException { + int startingSubState = subState; + int pos = buffer.position(); + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (subState >= 0) { + if (b == boundary[subState]) { + //if we have a potential boundary match + subState++; + if (subState == boundary.length) { + startingSubState = 0; + //we have our data + ByteBuffer retBuffer = buffer.duplicate(); + retBuffer.position(pos); + + retBuffer.limit(Math.max(buffer.position() - boundary.length, 0)); + encodingHandler.handle(partHandler, retBuffer); + partHandler.endPart(); + subState = -1; + } + } else if (b == boundary[0]) { + //we started half way through a boundary, but it turns out we did not actually meet the boundary condition + //so we call the part handler with our copy of the boundary data + if (startingSubState > 0) { + encodingHandler.handle(partHandler, ByteBuffer.wrap(boundary, 0, startingSubState)); + startingSubState = 0; + } + subState = 1; + } else { + //we started half way through a boundary, but it turns out we did not actually meet the boundary condition + //so we call the part handler with our copy of the boundary data + if (startingSubState > 0) { + encodingHandler.handle(partHandler, ByteBuffer.wrap(boundary, 0, startingSubState)); + startingSubState = 0; + } + subState = 0; + } + } else if (subState == -1) { + if (b == CR) { + subState = -2; + } else if (b == DASH) { + subState = -3; + } + } else if (subState == -2) { + if (b == LF) { + //ok, we have our data + subState = 0; + state = 1; + headers = new CaseInsensitiveMap(); + return; + } else if (b == DASH) { + subState = -3; + } else { + subState = -1; + } + } else if (subState == -3) { + if (b == DASH) { + state = -1; //we are done + return; + } else { + subState = -1; + } + } + } + //handle the data we read so far + ByteBuffer retBuffer = buffer.duplicate(); + retBuffer.position(pos); + if (subState == 0) { + //if we end partially through a boundary we do not handle the data + encodingHandler.handle(partHandler, retBuffer); + } else if (retBuffer.remaining() > subState && subState > 0) { + //we have some data to handle, and the end of the buffer might be a boundary match + retBuffer.limit(retBuffer.limit() - subState); + encodingHandler.handle(partHandler, retBuffer); + } + } + + public boolean isComplete() { + return state == -1; + } + } + + private interface Encoding { + void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException; + } + + private static class IdentityEncoding implements Encoding { + + @Override + public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException { + handler.data(rawData); + rawData.clear(); + } + } + + private static class Base64Encoding implements Encoding { + + private final Base64.Decoder decoder = Base64.getMimeDecoder(); + + @Override + public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException { + try { + ByteBuffer buf = decoder.decode(rawData); + handler.data(buf); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private static class QuotedPrintableEncoding implements Encoding { + + boolean equalsSeen; + byte firstCharacter; + + @Override + public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException { + boolean equalsSeen = this.equalsSeen; + byte firstCharacter = this.firstCharacter; + ByteBuffer buf = ByteBuffer.allocate(1024); + try { + while (rawData.hasRemaining()) { + byte b = rawData.get(); + if (equalsSeen) { + if (firstCharacter == 0) { + if (b == '\n' || b == '\r') { + //soft line break + //ignore + equalsSeen = false; + } else { + firstCharacter = b; + } + } else { + int result = Character.digit((char) firstCharacter, 16); + result <<= 4; //shift it 4 bytes and then add the next value to the end + result += Character.digit((char) b, 16); + buf.put((byte) result); + equalsSeen = false; + firstCharacter = 0; + } + } else if (b == '=') { + equalsSeen = true; + } else { + buf.put(b); + if (!buf.hasRemaining()) { + buf.flip(); + handler.data(buf); + buf.clear(); + } + } + } + buf.flip(); + handler.data(buf); + } finally { + this.equalsSeen = equalsSeen; + this.firstCharacter = firstCharacter; + } + } + } + + public static class MalformedMessageException extends RuntimeException { + } + +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/CustomServerRestHandlers.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/CustomServerRestHandlers.java index 1d0c7e2db1611..3d8458bf52fa2 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/CustomServerRestHandlers.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/CustomServerRestHandlers.java @@ -6,19 +6,13 @@ public final class CustomServerRestHandlers { private final Supplier blockingInputHandlerSupplier; - private final Supplier multipartHandlerSupplier; - public CustomServerRestHandlers(Supplier blockingInputHandlerSupplier, - Supplier multipartHandlerSupplier) { + public CustomServerRestHandlers(Supplier blockingInputHandlerSupplier) { this.blockingInputHandlerSupplier = blockingInputHandlerSupplier; - this.multipartHandlerSupplier = multipartHandlerSupplier; } public Supplier getBlockingInputHandlerSupplier() { return blockingInputHandlerSupplier; } - public Supplier getMultipartHandlerSupplier() { - return multipartHandlerSupplier; - } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index e7c3253b6a600..352724cdcff1f 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -219,18 +219,9 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } } // form params can be everywhere (field, beanparam, param) - if (method.isFormParamRequired() && !defaultBlocking) { + if (method.isFormParamRequired() || method.isMultipart()) { // read the body as multipart in one go - handlers.add(new FormBodyHandler(bodyParameter != null)); - } else if (method.isMultipart()) { - Supplier multipartHandlerSupplier = customServerRestHandlers.getMultipartHandlerSupplier(); - if (multipartHandlerSupplier != null) { - // multipart needs special body handling - handlers.add(multipartHandlerSupplier.get()); - } else { - throw new RuntimeException( - "The current execution environment does not implement a ServerRestHandler for multipart form support"); - } + handlers.add(new FormBodyHandler(bodyParameter != null, executorSupplier)); } else if (bodyParameter != null) { if (!defaultBlocking) { if (method.isBlocking()) { 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 3b721dec65b3a..12c1fe1ae9578 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 @@ -3,60 +3,129 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.ByteBuffer; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import org.jboss.resteasy.reactive.server.core.BlockingOperationSupport; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.FormDataParser; +import org.jboss.resteasy.reactive.server.core.multipart.FormEncodedDataDefinition; +import org.jboss.resteasy.reactive.server.core.multipart.FormParserFactory; +import org.jboss.resteasy.reactive.server.core.multipart.MultiPartParserDefinition; +import org.jboss.resteasy.reactive.server.spi.RuntimeConfigurableServerRestHandler; +import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; -public class FormBodyHandler implements ServerRestHandler { +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; - public FormBodyHandler(boolean alsoSetInputStream) { + public FormBodyHandler(boolean alsoSetInputStream, Supplier executorSupplier) { this.alsoSetInputStream = alsoSetInputStream; + this.executorSupplier = executorSupplier; + } + + @Override + public void configure(RuntimeConfiguration configuration) { + formParserFactory = FormParserFactory.builder(false, executorSupplier) + .addParser(new MultiPartParserDefinition(executorSupplier) + .setFileSizeThreshold(0) + .setMaxAttributeSize(configuration.limits().maxFormAttributeSize()) + .setMaxEntitySize(configuration.limits().maxBodySize().orElse(-1L)) + .setDeleteUploadsOnEnd(configuration.body().deleteUploadedFilesOnEnd()) + .setTempFileLocation(Path.of(configuration.body().uploadsDirectory()))) + .addParser(new FormEncodedDataDefinition() + .setMaxAttributeSize(configuration.limits().maxFormAttributeSize())) + .build(); + + try { + Files.createDirectories(Paths.get(configuration.body().uploadsDirectory())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { // in some cases, with sub-resource locators or via request filters, // it's possible we've already read the entity - if (requestContext.hasInputStream()) { + if (requestContext.getFormData() != null) { // let's not set it twice return; } ServerHttpRequest serverHttpRequest = requestContext.serverRequest(); - if (serverHttpRequest.isRequestEnded()) { + if (BlockingOperationSupport.isBlockingAllowed()) { + //blocking IO approach + + FormDataParser factory = formParserFactory.createParser(requestContext); + if (factory == null) { + return; + } + CapturingInputStream cis = null; if (alsoSetInputStream) { - // do not use the EmptyInputStream.INSTANCE marker - requestContext.setInputStream(new ByteArrayInputStream(NO_BYTES)); + // the TCK allows the body to be read as a form param and also as a body param + // the spec is silent about this + // TODO: this is really really horrible and hacky and needs to be fixed. + cis = new CapturingInputStream(requestContext.getInputStream()); + requestContext.setInputStream(cis); + } + factory.parseBlocking(); + if (alsoSetInputStream) { + requestContext.setInputStream(new ByteArrayInputStream(cis.baos.toByteArray())); + } + } else if (alsoSetInputStream) { + FormDataParser factory = formParserFactory.createParser(requestContext); + if (factory == null) { + return; } - } else { - serverHttpRequest.setExpectMultipart(true); requestContext.suspend(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - serverHttpRequest.setReadListener(new ServerHttpRequest.ReadCallback() { + executorSupplier.get().execute(new Runnable() { @Override - public void done() { - requestContext.setInputStream(new ByteArrayInputStream(outputStream.toByteArray())); - requestContext.resume(); - } - - @Override - public void data(ByteBuffer data) { - // the TCK allows the body to be read as a form param and also as a body param - // the spec is silent about this - // TODO: this is really really horrible and hacky and needs to be fixed. - byte[] buf = new byte[data.remaining()]; - data.get(buf); + public void run() { try { - outputStream.write(buf); - } catch (IOException ignored) { - //impossible + CapturingInputStream cis = new CapturingInputStream(requestContext.getInputStream()); + requestContext.setInputStream(cis); + factory.parseBlocking(); + requestContext.setInputStream(new ByteArrayInputStream(cis.baos.toByteArray())); + requestContext.resume(); + } catch (Throwable t) { + requestContext.resume(t); } } }); + } else { + FormDataParser factory = formParserFactory.createParser(requestContext); + if (factory == null) { + return; + } + //parse will auto resume + factory.parse(); + } + } + + static final class CapturingInputStream extends InputStream { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final InputStream delegate; + + CapturingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + int res = delegate.read(); + baos.write(res); + return res; } } 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 4f7d0fcad46cd..a60b187bc70d1 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 @@ -20,5 +20,7 @@ interface Body { interface Limits { Optional maxBodySize(); + + long maxFormAttributeSize(); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java index dab47b72d4bcf..ac7019ec75454 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java @@ -30,10 +30,6 @@ public interface ServerHttpRequest { void closeConnection(); - String getFormAttribute(String name); - - List getAllFormAttributes(String name); - String getQueryParam(String name); List getAllQueryParams(String name); @@ -44,8 +40,6 @@ public interface ServerHttpRequest { boolean isRequestEnded(); - void setExpectMultipart(boolean expectMultipart); - InputStream createInputStream(ByteBuffer existingData); InputStream createInputStream(); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxRequestContextFactory.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxRequestContextFactory.java index 25a0bcf19e93b..3352eb2967945 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxRequestContextFactory.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxRequestContextFactory.java @@ -14,6 +14,6 @@ public ResteasyReactiveRequestContext createContext(Deployment deployment, ProvidersImpl providers, Object context, ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain) { return new VertxResteasyReactiveRequestContext(deployment, providers, (RoutingContext) context, - requestContext, handlerChain, abortHandlerChain); + requestContext, handlerChain, abortHandlerChain, null); } } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java index 1f287b517ac45..ef343879a8fd2 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java @@ -39,16 +39,19 @@ public class VertxResteasyReactiveRequestContext extends ResteasyReactiveRequest protected final HttpServerRequest request; protected final HttpServerResponse response; private final Executor contextExecutor; + private final ClassLoader devModeTccl; protected Consumer preCommitTask; ContinueState continueState = ContinueState.NONE; public VertxResteasyReactiveRequestContext(Deployment deployment, ProvidersImpl providers, RoutingContext context, - ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain) { + ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain, + ClassLoader devModeTccl) { super(deployment, providers, requestContext, handlerChain, abortHandlerChain); this.context = context; this.request = context.request(); this.response = context.response(); + this.devModeTccl = devModeTccl; context.addHeadersEndHandler(this); String expect = request.getHeader(HttpHeaderNames.EXPECT); ContextInternal internal = ((ConnectionBase) context.request().connection()).getContext(); @@ -163,16 +166,6 @@ public void closeConnection() { response.close(); } - @Override - public String getFormAttribute(String name) { - return request.getFormAttribute(name); - } - - @Override - public List getAllFormAttributes(String name) { - return request.formAttributes().getAll(name); - } - @Override public String getQueryParam(String name) { return context.queryParams().get(name); @@ -198,11 +191,6 @@ public boolean isRequestEnded() { return request.isEnded(); } - @Override - public void setExpectMultipart(boolean expectMultipart) { - request.setExpectMultipart(expectMultipart); - } - @Override public InputStream createInputStream(ByteBuffer existingData) { if (existingData == null) { @@ -242,12 +230,18 @@ public ServerHttpResponse setReadListener(ReadCallback callback) { request.handler(new Handler() { @Override public void handle(Buffer event) { + if (devModeTccl != null) { + Thread.currentThread().setContextClassLoader(devModeTccl); + } callback.data(ByteBuffer.wrap(event.getBytes())); } }); request.endHandler(new Handler() { @Override public void handle(Void event) { + if (devModeTccl != null) { + Thread.currentThread().setContextClassLoader(devModeTccl); + } callback.done(); } });