From 4abf92ff97c7d946ffc2e18b2b57426d92e2d6ca Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 5 Dec 2019 21:34:18 -0300 Subject: [PATCH 1/2] [WIP] - Reusing request input stream --- .../keycloak/pep/runtime/VertxHttpFacade.java | 26 +++++++++++++++-- .../standalone/VertxRequestHandler.java | 10 +++++-- .../vertx/http/runtime}/VertxInputStream.java | 13 +++++++-- .../it/keycloak/ProtectedResource.java | 17 +++++++++++ .../src/main/resources/application.properties | 5 ++++ .../it/keycloak/PolicyEnforcerTest.java | 28 +++++++++++++++++++ 6 files changed, 93 insertions(+), 6 deletions(-) rename extensions/{resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone => vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime}/VertxInputStream.java (94%) diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java index 78a8b5fb934fa..825d0993b1306 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java @@ -11,6 +11,8 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.security.cert.X509Certificate; +import io.quarkus.vertx.http.runtime.VertxInputStream; +import io.vertx.core.http.HttpHeaders; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.OIDCHttpFacade; import org.keycloak.adapters.spi.AuthenticationError; @@ -107,7 +109,15 @@ public Cookie getCookie(String cookieName) { @Override public String getHeader(String name) { - return request.getHeader(name); + String value = request.getHeader(name); + + if (name.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE.toString())) { + if (value.indexOf(';') != -1) { + return value.substring(0, value.indexOf(';')); + } + } + + return value; } @Override @@ -122,7 +132,19 @@ public InputStream getInputStream() { @Override public InputStream getInputStream(boolean buffered) { - return new BufferedInputStream(new ByteArrayInputStream(routingContext.getBody().getBytes())); + try { + if (routingContext.get("quarkus.request.inputstream") != null) { + return routingContext.get("quarkus.request.inputstream"); + } + + BufferedInputStream stream = new BufferedInputStream(new VertxInputStream(request)); + + routingContext.put("quarkus.request.inputstream", stream); + + return stream; + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java index d676b8ab814c2..fb81b825c1ba0 100644 --- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java +++ b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java @@ -7,6 +7,7 @@ import javax.enterprise.inject.spi.CDI; import javax.ws.rs.core.SecurityContext; +import io.quarkus.vertx.http.runtime.VertxInputStream; import org.jboss.logging.Logger; import org.jboss.resteasy.core.ResteasyContext; import org.jboss.resteasy.core.SynchronousDispatcher; @@ -61,9 +62,14 @@ public VertxRequestHandler(Vertx vertx, public void handle(RoutingContext request) { // have to create input stream here. Cannot execute in another thread // otherwise request handlers may not get set up before request ends - VertxInputStream is; + InputStream is; try { - is = new VertxInputStream(request.request()); + if (request.get("quarkus.request.inputstream") != null) { + is = request.get("quarkus.request.inputstream"); + is.mark(0); + } else { + is = new VertxInputStream(request.request()); + } } catch (IOException e) { request.fail(e); return; diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxInputStream.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java similarity index 94% rename from extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxInputStream.java rename to extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java index 9c97bc82489f9..00cd2a2306386 100644 --- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxInputStream.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java @@ -1,4 +1,4 @@ -package io.quarkus.resteasy.runtime.standalone; +package io.quarkus.vertx.http.runtime; import java.io.IOException; import java.io.InputStream; @@ -10,6 +10,7 @@ import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; public class VertxInputStream extends InputStream { @@ -80,6 +81,7 @@ public int available() throws IOException { if (finished) { return -1; } + return exchange.readBytesAvailable(); } @@ -199,7 +201,14 @@ public int readBytesAvailable() { if (input1 != null) { return input1.getByteBuf().readableBytes(); } - return 0; + + String length = request.getHeader(HttpHeaders.CONTENT_LENGTH); + + if (length == null) { + return 0; + } + + return Integer.parseInt(length); } } diff --git a/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index a3ebd29a35ced..beb96fe320251 100644 --- a/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -1,20 +1,26 @@ package io.quarkus.it.keycloak; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import javax.inject.Inject; import javax.security.auth.AuthPermission; +import javax.ws.rs.Consumes; import javax.ws.rs.ForbiddenException; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import org.keycloak.representations.idm.authorization.Permission; import io.quarkus.security.identity.SecurityIdentity; +import io.vertx.core.http.HttpServerRequest; @Path("/api/permission") public class ProtectedResource { @@ -47,4 +53,15 @@ public List claimProtected() { public List httpResponseClaimProtected() { return identity.getAttribute("permissions"); } + + @Path("/body-claim") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public List bodyClaim(Map body, @Context HttpServerRequest request) { + if (body == null && !body.containsKey("from-body")) { + return Collections.emptyList(); + } + return identity.getAttribute("permissions"); + } } diff --git a/integration-tests/keycloak-authorization/src/main/resources/application.properties b/integration-tests/keycloak-authorization/src/main/resources/application.properties index 1aef8e6e968db..2989b9924ea5d 100644 --- a/integration-tests/keycloak-authorization/src/main/resources/application.properties +++ b/integration-tests/keycloak-authorization/src/main/resources/application.properties @@ -32,3 +32,8 @@ quarkus.keycloak.policy-enforcer.paths.3.claim-information-point.http.headers.Au # Disables policy enforcement for a path quarkus.keycloak.policy-enforcer.paths.4.path=/api/public quarkus.keycloak.policy-enforcer.paths.4.enforcement-mode=DISABLED + +# Defines a claim which value is based on the response from an external service +quarkus.keycloak.policy-enforcer.paths.5.path=/api/permission/body-claim +quarkus.keycloak.policy-enforcer.paths.5.claim-information-point.claims.from-body={request.body['/from-body']} + diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java index 0b87ae3d1a65f..8612f498e253a 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java @@ -26,6 +26,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import io.restassured.http.ContentType; /** * @author Pedro Igor @@ -116,6 +117,7 @@ private static ClientRepresentation createClient(String clientId) { configurePermissionResourcePermission(authorizationSettings); configureClaimBasedPermission(authorizationSettings); configureHttpResponseClaimBasedPermission(authorizationSettings); + configureBodyClaimBasedPermission(authorizationSettings); client.setAuthorizationSettings(authorizationSettings); @@ -155,6 +157,20 @@ private static void configureHttpResponseClaimBasedPermission(ResourceServerRepr "/api/permission/http-response-claim-protected"), policy); } + private static void configureBodyClaimBasedPermission(ResourceServerRepresentation settings) { + PolicyRepresentation policy = createJSPolicy("Body Claim-Based Policy", + "var context = $evaluation.getContext();\n" + + "print(context.getAttributes().toMap());" + + "var attributes = context.getAttributes();\n" + + "\n" + + "if (attributes.containsValue('from-body', 'grant')) {\n" + + " $evaluation.grant();\n" + + "}", + settings); + createPermission(settings, createResource(settings, "Body Claim Protected Resource", + "/api/permission/body-claim"), policy); + } + private static void createPermission(ResourceServerRepresentation settings, ResourceRepresentation resource, PolicyRepresentation policy) { PolicyRepresentation permission = new PolicyRepresentation(); @@ -260,6 +276,18 @@ public void testHttpResponseFromExternalServiceAsClaim() { .statusCode(403); } + @Test + public void testBodyClaim() { + RestAssured.given().auth().oauth2(getAccessToken("alice")) + .contentType(ContentType.JSON) + .body("{\"from-body\": \"grant\"}") + .when() + .post("/api/permission/body-claim") + .then() + .statusCode(200) + .and().body(Matchers.containsString("Body Claim Protected Resource")); + } + @Test public void testPublicResource() { RestAssured.given() From 2fb84157d91195367117b2117a21e684e3ceef32 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 6 Dec 2019 13:43:27 +1100 Subject: [PATCH 2/2] Use body handler to allow request buffering Fixes #5959 This change allows the body handler to be used for Undertow and RESTEasy standalone. When the request is fully buffered it can be consumed multiple times, which allows keycloak to also process it. --- bom/runtime/pom.xml | 2 +- .../KeycloakPolicyEnforcerBuildStep.java | 31 ++++++++++++++++ .../runtime/KeycloakPolicyEnforcerConfig.java | 36 +++++++++--------- .../keycloak/pep/runtime/VertxHttpFacade.java | 28 ++++---------- .../standalone/VertxRequestHandler.java | 8 ++-- .../runtime/UndertowDeploymentRecorder.java | 3 +- .../http/deployment/BodyHandlerBuildItem.java | 17 +++++++++ .../RequireBodyHandlerBuildItem.java | 10 +++++ .../http/deployment/VertxHttpProcessor.java | 19 +++++++++- .../vertx/http/runtime/VertxHttpRecorder.java | 37 ++++++++++++++++++- .../vertx/http/runtime/VertxInputStream.java | 8 ++-- .../web/deployment/BodyHandlerBuildItem.java | 4 ++ .../web/deployment/VertxWebProcessor.java | 12 +++--- .../vertx/web/runtime/VertxWebRecorder.java | 30 ++------------- 14 files changed, 161 insertions(+), 84 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/BodyHandlerBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireBodyHandlerBuildItem.java diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 32eff33ff677c..211a464ecf332 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -24,7 +24,7 @@ 0.2.0 0.0.12 0.34.0 - 3.0.0.Final + 3.0.1.Final 1.0.0.Final 1.3 1.0.1 diff --git a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java index f9775d62682b0..468ddc56e4e7a 100644 --- a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java +++ b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.keycloak.pep.deployment; +import java.util.Map; + import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.deployment.annotations.BuildStep; @@ -13,6 +15,7 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.runtime.OidcBuildTimeConfig; import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem; public class KeycloakPolicyEnforcerBuildStep { @@ -21,6 +24,34 @@ FeatureBuildItem featureBuildItem() { return new FeatureBuildItem(FeatureBuildItem.KEYCLOAK_AUTHORIZATION); } + @BuildStep + RequireBodyHandlerBuildItem requireBody(KeycloakPolicyEnforcerConfig config) { + if (config.policyEnforcer.enable) { + if (isBodyClaimInformationPointDefined(config.policyEnforcer.claimInformationPoint.simpleConfig)) { + return new RequireBodyHandlerBuildItem(); + } + for (KeycloakPolicyEnforcerConfig.KeycloakConfigPolicyEnforcer.PathConfig path : config.policyEnforcer.paths + .values()) { + if (isBodyClaimInformationPointDefined(path.claimInformationPoint.simpleConfig)) { + return new RequireBodyHandlerBuildItem(); + } + } + } + return null; + } + + private boolean isBodyClaimInformationPointDefined(Map> claims) { + for (Map.Entry> entry : claims.entrySet()) { + Map value = entry.getValue(); + + if (value.get(entry.getKey()).contains("request.body")) { + return true; + } + } + + return false; + } + @BuildStep public AdditionalBeanBuildItem beans(KeycloakPolicyEnforcerConfig config) { if (config.policyEnforcer.enable) { diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java index 9f6545124b69f..118ed1247c348 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java @@ -41,13 +41,13 @@ public static class KeycloakConfigPolicyEnforcer { * Specifies how policies are enforced. */ @ConfigItem(defaultValue = "ENFORCING") - String enforcementMode; + public String enforcementMode; /** * Specifies the paths to protect. */ @ConfigItem - Map paths; + public Map paths; /** * Defines how the policy enforcer should track associations between paths in your application and resources defined in @@ -56,7 +56,7 @@ public static class KeycloakConfigPolicyEnforcer { * protected resources */ @ConfigItem - PathCacheConfig pathCache; + public PathCacheConfig pathCache; /** * Specifies how the adapter should fetch the server for resources associated with paths in your application. If true, @@ -65,14 +65,14 @@ public static class KeycloakConfigPolicyEnforcer { * enforcer is going to fetch resources on-demand accordingly with the path being requested */ @ConfigItem(defaultValue = "true") - boolean lazyLoadPaths; + public boolean lazyLoadPaths; /** * Defines a set of one or more claims that must be resolved and pushed to the Keycloak server in order to make these * claims available to policies */ @ConfigItem - ClaimInformationPointConfig claimInformationPoint; + public ClaimInformationPointConfig claimInformationPoint; /** * Specifies how scopes should be mapped to HTTP methods. If set to true, the policy enforcer will use the HTTP method @@ -80,7 +80,7 @@ public static class KeycloakConfigPolicyEnforcer { * the current request to check whether or not access should be granted */ @ConfigItem - boolean httpMethodAsScope; + public boolean httpMethodAsScope; @ConfigGroup public static class PathConfig { @@ -89,13 +89,13 @@ public static class PathConfig { * The name of a resource on the server that is to be associated with a given path */ @ConfigItem - Optional name; + public Optional name; /** * A URI relative to the application’s context path that should be protected by the policy enforcer */ @ConfigItem - Optional path; + public Optional path; /** * The HTTP methods (for example, GET, POST, PATCH) to protect and how they are associated with the scopes for a @@ -103,14 +103,14 @@ public static class PathConfig { * resource in the server */ @ConfigItem - Map methods; + public Map methods; /** * Specifies how policies are enforced */ @DefaultConverter @ConfigItem(defaultValue = "ENFORCING") - PolicyEnforcerConfig.EnforcementMode enforcementMode; + public PolicyEnforcerConfig.EnforcementMode enforcementMode; /** * Defines a set of one or more claims that must be resolved and pushed to the Keycloak server in order to make @@ -118,7 +118,7 @@ public static class PathConfig { * claims available to policies */ @ConfigItem - ClaimInformationPointConfig claimInformationPoint; + public ClaimInformationPointConfig claimInformationPoint; } @ConfigGroup @@ -128,20 +128,20 @@ public static class MethodConfig { * The name of the HTTP method */ @ConfigItem - String method; + public String method; /** * An array of strings with the scopes associated with the method */ @ConfigItem - List scopes; + public List scopes; /** * A string referencing the enforcement mode for the scopes associated with a method */ @DefaultConverter @ConfigItem(defaultValue = "ALL") - PolicyEnforcerConfig.ScopeEnforcementMode scopesEnforcementMode; + public PolicyEnforcerConfig.ScopeEnforcementMode scopesEnforcementMode; } @ConfigGroup @@ -151,13 +151,13 @@ public static class PathCacheConfig { * Defines the time in milliseconds when the entry should be expired */ @ConfigItem(defaultValue = "1000") - int maxEntries = 1000; + public int maxEntries = 1000; /** * Defines the limit of entries that should be kept in the cache */ @ConfigItem(defaultValue = "30000") - long lifespan = 30000; + public long lifespan = 30000; } @ConfigGroup @@ -167,13 +167,13 @@ public static class ClaimInformationPointConfig { * */ @ConfigItem(name = ConfigItem.PARENT) - Map>> complexConfig; + public Map>> complexConfig; /** * */ @ConfigItem(name = ConfigItem.PARENT) - Map> simpleConfig; + public Map> simpleConfig; } } } diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java index 825d0993b1306..674c7daa8b476 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java @@ -1,6 +1,5 @@ package io.quarkus.keycloak.pep.runtime; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -11,8 +10,6 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.security.cert.X509Certificate; -import io.quarkus.vertx.http.runtime.VertxInputStream; -import io.vertx.core.http.HttpHeaders; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.OIDCHttpFacade; import org.keycloak.adapters.spi.AuthenticationError; @@ -25,6 +22,7 @@ import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.VertxInputStream; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServerRequest; @@ -109,15 +107,7 @@ public Cookie getCookie(String cookieName) { @Override public String getHeader(String name) { - String value = request.getHeader(name); - - if (name.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE.toString())) { - if (value.indexOf(';') != -1) { - return value.substring(0, value.indexOf(';')); - } - } - - return value; + return request.getHeader(name); } @Override @@ -133,15 +123,13 @@ public InputStream getInputStream() { @Override public InputStream getInputStream(boolean buffered) { try { - if (routingContext.get("quarkus.request.inputstream") != null) { - return routingContext.get("quarkus.request.inputstream"); + if (routingContext.getBody() != null) { + return new ByteArrayInputStream(routingContext.getBody().getBytes()); } - - BufferedInputStream stream = new BufferedInputStream(new VertxInputStream(request)); - - routingContext.put("quarkus.request.inputstream", stream); - - return stream; + if (routingContext.request().isEnded()) { + return new ByteArrayInputStream(new byte[0]); + } + return new VertxInputStream(request); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java index fb81b825c1ba0..ccd63565410d4 100644 --- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java +++ b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxRequestHandler.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.runtime.standalone; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -7,7 +8,6 @@ import javax.enterprise.inject.spi.CDI; import javax.ws.rs.core.SecurityContext; -import io.quarkus.vertx.http.runtime.VertxInputStream; import org.jboss.logging.Logger; import org.jboss.resteasy.core.ResteasyContext; import org.jboss.resteasy.core.SynchronousDispatcher; @@ -20,6 +20,7 @@ import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.quarkus.vertx.http.runtime.VertxInputStream; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.vertx.core.Context; import io.vertx.core.Handler; @@ -64,9 +65,8 @@ public void handle(RoutingContext request) { // otherwise request handlers may not get set up before request ends InputStream is; try { - if (request.get("quarkus.request.inputstream") != null) { - is = request.get("quarkus.request.inputstream"); - is.mark(0); + if (request.getBody() != null) { + is = new ByteArrayInputStream(request.getBody().getBytes()); } else { is = new VertxInputStream(request.request()); } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index f9e7394440a1b..d66f8d71f3e48 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -353,7 +353,8 @@ public void run() { return new Handler() { @Override public void handle(RoutingContext event) { - VertxHttpExchange exchange = new VertxHttpExchange(event.request(), allocator, executorService, event); + VertxHttpExchange exchange = new VertxHttpExchange(event.request(), allocator, executorService, event, + event.getBody()); Optional maxBodySize = httpConfiguration.limits.maxBodySize; if (maxBodySize.isPresent()) { exchange.setMaxEntitySize(maxBodySize.get().asLongValue()); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/BodyHandlerBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/BodyHandlerBuildItem.java new file mode 100644 index 0000000000000..c5743fcd30385 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/BodyHandlerBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.vertx.http.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public final class BodyHandlerBuildItem extends SimpleBuildItem { + private final Handler handler; + + public BodyHandlerBuildItem(Handler handler) { + this.handler = handler; + } + + public Handler getHandler() { + return handler; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireBodyHandlerBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireBodyHandlerBuildItem.java new file mode 100644 index 0000000000000..dbfd8541499a3 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireBodyHandlerBuildItem.java @@ -0,0 +1,10 @@ +package io.quarkus.vertx.http.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * This is a marker that indicates that the body handler should be installed + * on all routes, as an extension requires the request to be fully buffered. + */ +public final class RequireBodyHandlerBuildItem extends MultiBuildItem { +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 2dcebfca9208d..3f79ed6ee306c 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -35,8 +35,10 @@ import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.quarkus.vertx.http.runtime.cors.CORSRecorder; import io.quarkus.vertx.http.runtime.filters.Filter; +import io.vertx.core.Handler; import io.vertx.core.impl.VertxImpl; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; class VertxHttpProcessor { @@ -97,6 +99,12 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, return new VertxWebRouterBuildItem(router); } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + BodyHandlerBuildItem bodyHandler(VertxHttpRecorder recorder, HttpConfiguration httpConfiguration) { + return new BodyHandlerBuildItem(recorder.createBodyHandler(httpConfiguration)); + } + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) ServiceStartBuildItem finalizeRouter( @@ -106,7 +114,9 @@ ServiceStartBuildItem finalizeRouter( List defaultRoutes, List filters, VertxWebRouterBuildItem router, EventLoopCountBuildItem eventLoopCount, HttpBuildTimeConfig httpBuildTimeConfig, HttpConfiguration httpConfiguration, - BuildProducer reflectiveClass, List websocketSubProtocols) + BuildProducer reflectiveClass, List websocketSubProtocols, + List requireBodyHandlerBuildItems, + BodyHandlerBuildItem bodyHandlerBuildItem) throws BuildException, IOException { Optional defaultRoute; if (defaultRoutes == null || defaultRoutes.isEmpty()) { @@ -124,9 +134,14 @@ ServiceStartBuildItem finalizeRouter( .filter(f -> f.getHandler() != null) .map(FilterBuildItem::toFilter).collect(Collectors.toList()); + //if the body handler is required then we know it is installed for all routes, so we don't need to register it here + Handler bodyHandler = !requireBodyHandlerBuildItems.isEmpty() ? bodyHandlerBuildItem.getHandler() + : null; + recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), - listOfFilters, vertx.getVertx(), router.getRouter(), httpBuildTimeConfig.rootPath, launchMode.getLaunchMode()); + listOfFilters, vertx.getVertx(), router.getRouter(), httpBuildTimeConfig.rootPath, launchMode.getLaunchMode(), + !requireBodyHandlerBuildItems.isEmpty(), bodyHandler); boolean startVirtual = requireVirtual.isPresent() || httpBuildTimeConfig.virtual; if (startVirtual) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 4b7b13dfbc37a..69568666ea191 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -35,6 +35,7 @@ import io.quarkus.runtime.Timing; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; +import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.vertx.core.runtime.VertxCoreRecorder; import io.quarkus.vertx.core.runtime.config.VertxConfiguration; import io.quarkus.vertx.http.runtime.filters.Filter; @@ -61,6 +62,7 @@ import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; @Recorder public class VertxHttpRecorder { @@ -169,7 +171,8 @@ public void startServer(RuntimeValue vertxRuntimeValue, ShutdownContext s public void finalizeRouter(BeanContainer container, Consumer defaultRouteHandler, List filterList, RuntimeValue vertx, - RuntimeValue runtimeValue, String rootPath, LaunchMode launchMode) { + RuntimeValue runtimeValue, String rootPath, LaunchMode launchMode, boolean requireBodyHandler, + Handler bodyHandler) { // install the default route at the end Router router = runtimeValue.getValue(); @@ -200,6 +203,18 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute container.instance(RouterProducer.class).initialize(resumingRouter); router.route().last().failureHandler(new QuarkusErrorHandler(launchMode.isDevOrTest())); + if (requireBodyHandler) { + //if this is set then everything needs the body handler installed + //TODO: config etc + router.route().order(Integer.MIN_VALUE).handler(new Handler() { + @Override + public void handle(RoutingContext routingContext) { + routingContext.request().resume(); + bodyHandler.handle(routingContext); + } + }); + } + if (rootPath.equals("/")) { if (hotReplacementHandler != null) { router.route().order(-1).handler(hotReplacementHandler); @@ -626,4 +641,24 @@ public static Handler getRootHandler() { return ACTUAL_ROOT; } + public Handler createBodyHandler(HttpConfiguration httpConfiguration) { + BodyHandler bodyHandler = BodyHandler.create(); + Optional maxBodySize = httpConfiguration.limits.maxBodySize; + if (maxBodySize.isPresent()) { + bodyHandler.setBodyLimit(maxBodySize.get().asLongValue()); + } + final BodyConfig bodyConfig = httpConfiguration.body; + bodyHandler.setHandleFileUploads(bodyConfig.handleFileUploads); + bodyHandler.setUploadsDirectory(bodyConfig.uploadsDirectory); + bodyHandler.setDeleteUploadedFilesOnEnd(bodyConfig.deleteUploadedFilesOnEnd); + bodyHandler.setMergeFormAttributes(bodyConfig.mergeFormAttributes); + bodyHandler.setPreallocateBodyBuffer(bodyConfig.preallocateBodyBuffer); + return new Handler() { + @Override + public void handle(RoutingContext event) { + event.request().resume(); + bodyHandler.handle(event); + } + }; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java index 00cd2a2306386..357f77de923aa 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxInputStream.java @@ -81,7 +81,7 @@ public int available() throws IOException { if (finished) { return -1; } - + return exchange.readBytesAvailable(); } @@ -201,13 +201,13 @@ public int readBytesAvailable() { if (input1 != null) { return input1.getByteBuf().readableBytes(); } - + String length = request.getHeader(HttpHeaders.CONTENT_LENGTH); - + if (length == null) { return 0; } - + return Integer.parseInt(length); } } diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/BodyHandlerBuildItem.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/BodyHandlerBuildItem.java index 715dbafcde91b..d8e3e968eef7b 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/BodyHandlerBuildItem.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/BodyHandlerBuildItem.java @@ -4,6 +4,10 @@ import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; +/** + * use {@link io.quarkus.vertx.http.deployment.BodyHandlerBuildItem} instead + */ +@Deprecated public final class BodyHandlerBuildItem extends SimpleBuildItem { private final Handler handler; diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java index 8b98af3df949c..ed69c72f5731e 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java @@ -48,9 +48,9 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.vertx.http.deployment.FilterBuildItem; +import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; -import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.web.Route; import io.quarkus.vertx.web.RouteBase; import io.quarkus.vertx.web.RouteFilter; @@ -149,9 +149,8 @@ void validateBeanDeployment( } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - BodyHandlerBuildItem bodyHandler(VertxWebRecorder recorder, HttpConfiguration httpConfiguration) { - return new BodyHandlerBuildItem(recorder.createBodyHandler(httpConfiguration)); + BodyHandlerBuildItem bodyHandler(io.quarkus.vertx.http.deployment.BodyHandlerBuildItem realOne) { + return new BodyHandlerBuildItem(realOne.getHandler()); } @BuildStep @@ -163,9 +162,10 @@ void addAdditionalRoutes( BuildProducer generatedClass, AnnotationProxyBuildItem annotationProxy, BuildProducer reflectiveClasses, - BodyHandlerBuildItem bodyHandler, + io.quarkus.vertx.http.deployment.BodyHandlerBuildItem bodyHandler, BuildProducer routeProducer, - BuildProducer filterProducer) throws IOException { + BuildProducer filterProducer, + List bodyHandlerRequired) throws IOException { ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClass, true); diff --git a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java index ffc08969aa4fb..e7b3051717b6b 100644 --- a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java +++ b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java @@ -1,20 +1,15 @@ package io.quarkus.vertx.web.runtime; import java.lang.reflect.InvocationTargetException; -import java.util.Optional; import java.util.function.Function; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.MemorySize; -import io.quarkus.vertx.http.runtime.BodyConfig; -import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.RouterProducer; import io.quarkus.vertx.web.Route; import io.vertx.core.Handler; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; @Recorder public class VertxWebRecorder { @@ -66,33 +61,14 @@ public io.vertx.ext.web.Route apply(Router router) { route.consumes(consumes); } } - route.handler(bodyHandler); + if (bodyHandler != null) { + route.handler(bodyHandler); + } return route; } }; } - public Handler createBodyHandler(HttpConfiguration httpConfiguration) { - BodyHandler bodyHandler = BodyHandler.create(); - Optional maxBodySize = httpConfiguration.limits.maxBodySize; - if (maxBodySize.isPresent()) { - bodyHandler.setBodyLimit(maxBodySize.get().asLongValue()); - } - final BodyConfig bodyConfig = httpConfiguration.body; - bodyHandler.setHandleFileUploads(bodyConfig.handleFileUploads); - bodyHandler.setUploadsDirectory(bodyConfig.uploadsDirectory); - bodyHandler.setDeleteUploadedFilesOnEnd(bodyConfig.deleteUploadedFilesOnEnd); - bodyHandler.setMergeFormAttributes(bodyConfig.mergeFormAttributes); - bodyHandler.setPreallocateBodyBuffer(bodyConfig.preallocateBodyBuffer); - return new Handler() { - @Override - public void handle(RoutingContext event) { - event.request().resume(); - bodyHandler.handle(event); - } - }; - } - private String ensureStartWithSlash(String path) { if (path.startsWith("/")) { return path;