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/devui/deployment/DevUIConfig.java similarity index 91% rename from extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevUIConfig.java rename to extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIConfig.java index 828db66ef474c..9b9415a33688d 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/devui/deployment/DevUIConfig.java @@ -1,4 +1,4 @@ -package io.quarkus.vertx.http.deployment.devmode.console; +package io.quarkus.devui.deployment; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java index f57d61b6f898b..c995e43333b4f 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -63,11 +63,13 @@ import io.quarkus.maven.dependency.GACTV; import io.quarkus.qute.Qute; import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; import io.quarkus.vertx.http.deployment.webjar.WebJarResourcesFilter; import io.quarkus.vertx.http.deployment.webjar.WebJarResultsBuildItem; +import io.quarkus.vertx.http.runtime.devmode.DevConsoleCORSFilter; import io.smallrye.common.annotation.Blocking; import io.smallrye.common.annotation.NonBlocking; import io.smallrye.mutiny.Multi; @@ -120,6 +122,7 @@ public class DevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) @Record(ExecutionTime.STATIC_INIT) void registerDevUiHandlers( + DevUIConfig devUIConfig, MvnpmBuildItem mvnpmBuildItem, List devUIRoutesBuildItems, List staticContentBuildItems, @@ -133,6 +136,13 @@ void registerDevUiHandlers( return; } + if (devUIConfig.cors.enabled) { + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .orderedRoute(DEVUI + SLASH_ALL, -1 * FilterBuildItem.CORS) + .handler(new DevConsoleCORSFilter()) + .build()); + } + // Websocket for JsonRPC comms routeProducer.produce( nonApplicationRootPathBuildItem @@ -155,8 +165,8 @@ void registerDevUiHandlers( .route(route) .handler(uihandler); - if (route.endsWith(DEVUI)) { - builder = builder.displayOnNotFoundPage("Dev UI 2.0"); + if (route.endsWith(DEVUI + SLASH)) { + builder = builder.displayOnNotFoundPage("Dev UI (v2)"); routeProducer.produce(builder.build()); } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java index c39040cd9cf7d..67ecf5846ef9a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java @@ -273,7 +273,23 @@ public Builder routeFunction(Function routeFunction) { } public Builder routeFunction(String route, Consumer routeFunction) { + return orderedRoute(route, null, routeFunction); + } + + @Override + public Builder route(String route) { + routeFunction(route, null); + return this; + } + + @Override + public Builder orderedRoute(String route, Integer order) { + orderedRoute(route, order, null); + return this; + } + @Override + public Builder orderedRoute(String route, Integer order, Consumer routeFunction) { if (isManagement && this.buildItem.managementRootPath != null) { // The logic is slightly different when the management interface is enabled, as we have a single // router mounted at the root. @@ -283,7 +299,7 @@ public Builder routeFunction(String route, Consumer routeFunction) { this.path = buildItem.getManagementRootPath() + route; } this.routerType = RouteBuildItem.RouteType.ABSOLUTE_ROUTE; - super.routeFunction(this.path, routeFunction); + super.orderedRoute(this.path, order, routeFunction); return this; } @@ -304,14 +320,7 @@ public Builder routeFunction(String route, Consumer routeFunction) { this.path = route; this.routerType = RouteBuildItem.RouteType.ABSOLUTE_ROUTE; } - - super.routeFunction(this.path, routeFunction); - return this; - } - - @Override - public Builder route(String route) { - routeFunction(route, null); + super.orderedRoute(this.path, order, routeFunction); return this; } @@ -401,6 +410,7 @@ public Builder management() { super.management(); return this; } + } /** 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 ef12ca50856a9..f70d7def67025 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 @@ -65,6 +65,7 @@ import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; import io.quarkus.devconsole.spi.DevConsoleWebjarBuildItem; +import io.quarkus.devui.deployment.DevUIConfig; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.GACT; import io.quarkus.netty.runtime.virtual.VirtualChannel; @@ -90,6 +91,7 @@ import io.quarkus.runtime.TemplateHtmlBuilder; import io.quarkus.utilities.OS; import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; @@ -477,12 +479,6 @@ 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.cors.enabled) { - routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() - .route("dev-v1/*") - .handler(new DevConsoleCORSFilter()) - .build()); - } NonApplicationRootPathBuildItem.Builder builder = nonApplicationRootPathBuildItem.routeBuilder() .routeFunction( "dev-v1/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath(), @@ -496,6 +492,13 @@ public void setupDevConsoleRoutes( } } + if (devUIConfig.cors.enabled) { + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .orderedRoute("dev-v1/*", -1 * FilterBuildItem.CORS) + .handler(new DevConsoleCORSFilter()) + .build()); + } + DevConsoleManager.registerHandler(new DevConsoleHttpHandler()); //must be last so the above routes have precedence routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devui/DevUICorsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devui/DevUICorsTest.java new file mode 100644 index 0000000000000..f17f7e5f636c1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devui/DevUICorsTest.java @@ -0,0 +1,194 @@ +package io.quarkus.vertx.http.devui; + +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 DevUICorsTest { + + @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-ui/configuration-form-editor").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", methods) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightHttpLocalhostIpOrigin() { + String origin = "http://127.0.0.1:8080"; + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev-ui/configuration-form-editor").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:8443"; + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev-ui/configuration-form-editor").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", methods) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightHttpsLocalhostIpOrigin() { + String origin = "https://127.0.0.1:8443"; + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev-ui/configuration-form-editor").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-ui/configuration-form-editor").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()) + .header("Access-Control-Allow-Methods", nullValue()) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightBadLocalhostOrigin() { + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", "http://localhost:8080/devui") + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev-ui/configuration-form-editor").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()) + .body(emptyOrNullString()); + } + + @Test + public void testPreflightBadLocalhostIpOrigin() { + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", "http://127.0.0.1:8080/devui") + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev-ui/configuration-form-editor").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", 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-ui/configuration-form-editor").then() + .statusCode(403) + .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-ui/configuration-form-editor").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", nullValue()) + .body(not(emptyOrNullString())); + } + + @Test + public void testSimpleRequestHttpLocalhostIpOrigin() { + String origin = "http://127.0.0.1:8080"; + RestAssured.given() + .header("Origin", origin) + .when() + .get("q/dev-ui/configuration-form-editor").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:8443"; + RestAssured.given() + .header("Origin", origin) + .when() + .get("q/dev-ui/configuration-form-editor").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", nullValue()) + .body(not(emptyOrNullString())); + } + + @Test + public void testSimpleRequestHttpsLocalhostIpOrigin() { + String origin = "https://127.0.0.1:8443"; + RestAssured.given() + .header("Origin", origin) + .when() + .get("q/dev-ui/configuration-form-editor").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", nullValue()) + .body(not(emptyOrNullString())); + } + + @Test + public void testSimpleRequestNonLocalhostOrigin() { + RestAssured.given() + .header("Origin", "https://quarkus.io/http://localhost") + .when() + .get("q/dev-ui/configuration-form-editor").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()) + .body(emptyOrNullString()); + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devui/DevUIRemoteCorsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devui/DevUIRemoteCorsTest.java new file mode 100644 index 0000000000000..b33d1009d119c --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devui/DevUIRemoteCorsTest.java @@ -0,0 +1,37 @@ +package io.quarkus.vertx.http.devui; + +import static org.hamcrest.Matchers.emptyOrNullString; + +import java.net.Inet4Address; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevUIRemoteCorsTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .setBuildSystemProperty("quarkus.http.host", "0.0.0.0") + .withEmptyApplication(); + + @Test + public void test() throws UnknownHostException { + String origin = Inet4Address.getLocalHost().toString(); + if (origin.contains("/")) { + origin = "http://" + origin.split("/")[1] + ":8080"; + } + String methods = "GET,POST"; + RestAssured.given() + .header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .when() + .options("q/dev-ui/configuration-form-editor").then() + .statusCode(403) + .body(emptyOrNullString()); + } + +} diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js index 36cc482fe1e84..88f8a1b92a742 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js @@ -18,6 +18,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; import { observeState } from 'lit-element-state'; import { devuiState } from 'devui-state'; +import { connectionState } from 'connection-state'; import 'qui-badge'; /** @@ -107,6 +108,8 @@ export class QwcConfiguration extends observeState(LitElement) { render() { if (this._filtered && this._values) { return this._render(); + } else if(!connectionState.current.isConnected){ + return html`Waiting for backend connection...`; } else { return html`Loading configuration properties...`; }