From 7821d4f340bfd262e86c332f75d159898a4a2420 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 17 Nov 2022 10:10:01 +0000 Subject: [PATCH] Add CORS route to DevConsole --- .../devmode/console/DevConsoleProcessor.java | 8 ++ .../devmode/console/DevUIConfig.java | 6 ++ .../http/devconsole/DevConsoleCorsTest.java | 102 ++++++++++++++++++ .../vertx/http/runtime/cors/CORSConfig.java | 12 +-- .../runtime/devmode/DevConsoleCORSFilter.java | 51 +++++++++ 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/DevConsoleCorsTest.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleCORSFilter.java diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index 6c34e448daabe..a54d0c8b69f3e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -95,6 +95,7 @@ import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; import io.quarkus.vertx.http.deployment.webjar.WebJarResultsBuildItem; +import io.quarkus.vertx.http.runtime.devmode.DevConsoleCORSFilter; import io.quarkus.vertx.http.runtime.devmode.DevConsoleFilter; import io.quarkus.vertx.http.runtime.devmode.DevConsoleRecorder; import io.quarkus.vertx.http.runtime.devmode.RedirectHandler; @@ -438,6 +439,7 @@ public DevConsoleTemplateInfoBuildItem config(List routes, @@ -472,6 +474,12 @@ public void setupDevConsoleRoutes( // if the handler is a proxy, then that means it's been produced by a recorder and therefore belongs in the regular runtime Vert.x instance // otherwise this is handled in the setupDeploymentSideHandling method if (!i.isDeploymentSide()) { + if (devUIConfig.corsEnabled) { + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("dev/*") + .handler(new DevConsoleCORSFilter()) + .build()); + } NonApplicationRootPathBuildItem.Builder builder = nonApplicationRootPathBuildItem.routeBuilder() .routeFunction( "dev/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath(), diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevUIConfig.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevUIConfig.java index 634a1d9d7dd5a..aa5e5cd649fb3 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevUIConfig.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevUIConfig.java @@ -12,4 +12,10 @@ public class DevUIConfig { @ConfigItem(defaultValue = "50") public int historySize; + /** + * Enable CORS filter. + */ + @ConfigItem(defaultValue = "true") + public boolean corsEnabled = true; + } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/DevConsoleCorsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/DevConsoleCorsTest.java new file mode 100644 index 0000000000000..b1fb3126b2d84 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/DevConsoleCorsTest.java @@ -0,0 +1,102 @@ +package io.quarkus.vertx.http.devconsole; + +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevConsoleCorsTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withEmptyApplication(); + + @Test + public void testPreflightHttpLocalhostOrigin() { + String origin = "http://localhost:8080"; + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev/io.quarkus.quarkus-vertx-http/config").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", methods) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightHttpsLocalhostOrigin() { + String origin = "https://localhost:8080"; + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev/io.quarkus.quarkus-vertx-http/config").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", methods) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightNonLocalhostOrigin() { + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", "https://quarkus.io/http://localhost") + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev/io.quarkus.quarkus-vertx-http/config").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", nullValue()) + .header("Access-Control-Allow-Methods", nullValue()) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightLocalhostOriginWithoutPort() { + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", "http://localhost") + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev/io.quarkus.quarkus-vertx-http/config").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", nullValue()) + .body(emptyOrNullString()); + } + + @Test + public void testSimpleRequestHttpLocalhostOrigin() { + String origin = "http://localhost:8080"; + RestAssured.given() + .header("Origin", origin) + .when() + .get("q/dev/io.quarkus.quarkus-vertx-http/config").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", nullValue()) + .body(not(emptyOrNullString())); + } + + @Test + public void testSimpleRequestHttpsLocalhostOrigin() { + String origin = "https://localhost:8080"; + RestAssured.given() + .header("Origin", origin) + .when() + .get("q/dev/io.quarkus.quarkus-vertx-http/config").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", nullValue()) + .body(not(emptyOrNullString())); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java index 50f45d0b1f383..d98820d59c099 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java @@ -24,7 +24,7 @@ public class CORSConfig { */ @ConfigItem @ConvertWith(TrimmedStringConverter.class) - public Optional> origins; + public Optional> origins = Optional.empty(); /** * HTTP methods allowed for CORS @@ -36,7 +36,7 @@ public class CORSConfig { */ @ConfigItem @ConvertWith(TrimmedStringConverter.class) - public Optional> methods; + public Optional> methods = Optional.empty(); /** * HTTP headers allowed for CORS @@ -48,7 +48,7 @@ public class CORSConfig { */ @ConfigItem @ConvertWith(TrimmedStringConverter.class) - public Optional> headers; + public Optional> headers = Optional.empty(); /** * HTTP headers exposed in CORS @@ -59,14 +59,14 @@ public class CORSConfig { */ @ConfigItem @ConvertWith(TrimmedStringConverter.class) - public Optional> exposedHeaders; + public Optional> exposedHeaders = Optional.empty(); /** * The `Access-Control-Max-Age` response header value indicating * how long the results of a pre-flight request can be cached. */ @ConfigItem - public Optional accessControlMaxAge; + public Optional accessControlMaxAge = Optional.empty(); /** * The `Access-Control-Allow-Credentials` header is used to tell the @@ -77,7 +77,7 @@ public class CORSConfig { * there is a match with the precise `Origin` header and that header is not '*'. */ @ConfigItem - public Optional accessControlAllowCredentials; + public Optional accessControlAllowCredentials = Optional.empty(); @Override public String toString() { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleCORSFilter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleCORSFilter.java new file mode 100644 index 0000000000000..70889ed606129 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleCORSFilter.java @@ -0,0 +1,51 @@ +package io.quarkus.vertx.http.runtime.devmode; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.vertx.http.runtime.cors.CORSConfig; +import io.quarkus.vertx.http.runtime.cors.CORSFilter; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; + +public class DevConsoleCORSFilter implements Handler { + private static final Logger LOG = Logger.getLogger(DevConsoleCORSFilter.class); + + private static final String HTTP_PORT_CONFIG_PROP = "quarkus.http.port"; + private static final String LOCAL_HOST = "localhost"; + private static final String HTTP_LOCAL_HOST = "http://" + LOCAL_HOST; + private static final String HTTPS_LOCAL_HOST = "https://" + LOCAL_HOST; + + public DevConsoleCORSFilter() { + } + + private static CORSFilter corsFilter() { + int httpPort = ConfigProvider.getConfig().getValue(HTTP_PORT_CONFIG_PROP, int.class); + CORSConfig config = new CORSConfig(); + config.origins = Optional.of(List.of(HTTP_LOCAL_HOST + ":" + httpPort, HTTPS_LOCAL_HOST + ":" + httpPort)); + return new CORSFilter(config); + } + + @Override + public void handle(RoutingContext event) { + HttpServerRequest request = event.request(); + HttpServerResponse response = event.response(); + String origin = request.getHeader(HttpHeaders.ORIGIN); + if (origin == null) { + corsFilter().handle(event); + } else { + if (origin.startsWith(HTTP_LOCAL_HOST) || origin.startsWith(HTTPS_LOCAL_HOST)) { + corsFilter().handle(event); + } else { + LOG.error("Only localhost origin is allowed"); + response.end(); + } + } + } +}