From 70b65a2ed3ca7ddb5d71a7d2ee4be0c10962dbed Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Fri, 20 Jan 2023 14:39:17 +0100 Subject: [PATCH] Adds management interface support. It allows exposing selected routes (management routes) to a different HTTP server. It avoids exposing these management routes on the main HTTP server, which could lead to leaks and undesired access to these endpoints. Enabling/Disabling the management interface is a build-time property. However, the interface, port, and SSL... are runtime values. The management interface is not intended to be used using native transport (as high concurrency is rarely a need for these endpoints). Also, the access log and same site cookie are not supported yet. The management interface does not expose plain and secured endpoints. It's either using HTTP or HTTPS. At the moment are considered management routes: * health routes (but not the health-ui) * prometheus routes * metrics routes The management interface is, when enabled, exposed on the port 9000 (9001 in test mode). --- .github/native-tests.json | 4 +- docs/src/main/asciidoc/http-reference.adoc | 2 +- .../management-interface-reference.adoc | 163 ++++ .../kind/deployment/KindProcessor.java | 2 +- .../deployment/MinikubeProcessor.java | 2 +- .../kubernetes/vanilla/deployment/pom.xml | 4 + .../deployment/DevClusterHelper.java | 2 +- .../deployment/KnativeProcessor.java | 2 +- .../deployment/KubernetesCommonHelper.java | 2 +- .../deployment/OpenshiftProcessor.java | 2 +- .../VanillaKubernetesProcessor.java | 2 +- .../export/PrometheusRegistryProcessor.java | 4 + ...theusEnabledOnManagementInterfaceTest.java | 35 + .../test/devconsole/BodyHandlerBean.java | 4 +- .../deployment/SmallRyeHealthProcessor.java | 35 +- ...heckOnManagementInterfaceDisabledTest.java | 57 ++ .../HealthCheckOnManagementInterfaceTest.java | 58 ++ ...mentInterfaceWithAbsoluteRootPathTest.java | 58 ++ ...mentInterfaceWithRelativeRootPathTest.java | 59 ++ .../deployment/SmallRyeMetricsProcessor.java | 2 + .../spi/UseManagementInterfaceBuildItem.java | 10 + .../deployment/HttpRootPathBuildItem.java | 8 +- .../NonApplicationRootPathBuildItem.java | 149 +++- .../vertx/http/deployment/RouteBuildItem.java | 27 +- .../http/deployment/VertxHttpProcessor.java | 51 +- .../deployment/VertxWebRouterBuildItem.java | 15 +- .../devmode/console/ConfiguredPathInfo.java | 13 +- .../devmode/console/DevConsoleProcessor.java | 9 +- .../http/NonApplicationAndRootPathTest.java | 3 +- .../NonApplicationRootPathBuildItemTest.java | 168 +++- .../http/devconsole/BodyHandlerBean.java | 4 +- .../management/ManagementAndRootPathTest.java | 82 ++ .../management/ManagementWithJksTest.java | 89 ++ .../ManagementWithMainServerDisabledTest.java | 81 ++ .../management/ManagementWithP12Test.java | 89 ++ .../management/ManagementWithPemTest.java | 90 +++ .../http/runtime/ForwardedProxyHandler.java | 4 +- .../ForwardedServerRequestWrapper.java | 4 +- .../http/runtime/ForwardingProxyOptions.java | 24 +- .../http/runtime/ResumingRequestWrapper.java | 4 +- .../vertx/http/runtime/TrustedProxyCheck.java | 2 +- .../vertx/http/runtime/VertxHttpRecorder.java | 764 ++++++++---------- .../ManagementInterfaceBuildTimeConfig.java | 68 ++ .../ManagementInterfaceConfiguration.java | 120 +++ .../options/HttpServerCommonHandlers.java | 177 ++++ .../options/HttpServerOptionsUtils.java | 386 +++++++++ .../management-interface/pom.xml | 102 +++ .../it/management/GreetingResource.java | 13 + .../src/main/resources/application.properties | 1 + .../it/management/ManagementInterfaceIT.java | 12 + .../ManagementInterfaceTestCase.java | 33 + integration-tests/pom.xml | 1 + 52 files changed, 2586 insertions(+), 516 deletions(-) create mode 100644 docs/src/main/asciidoc/management-interface-reference.adoc create mode 100644 extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java create mode 100644 extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java create mode 100644 integration-tests/management-interface/pom.xml create mode 100644 integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java create mode 100644 integration-tests/management-interface/src/main/resources/application.properties create mode 100644 integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java create mode 100644 integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java diff --git a/.github/native-tests.json b/.github/native-tests.json index 64657bc2c1bb86..269364ba6c5df1 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -92,8 +92,8 @@ }, { "category": "HTTP", - "timeout": 90, - "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart", + "timeout": 95, + "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, management-interface", "os-name": "ubuntu-latest" }, { diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index cc1fed445383b4..ec35b6f9c9d2d6 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -261,7 +261,7 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-filter-config.ad == Support 100-Continue in vert.x In order to support `100-continue`, the `quarkus.http.handle-100-continue-automatically` option needs to be enabled explicitly -For additional information check https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1[100-continue= and the related +For additional information check https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1[100-continue] and the related https://vertx.io/docs/apidocs/io/vertx/core/http/HttpServerOptions.html#DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY[Vert.x documentation]. [source,properties] diff --git a/docs/src/main/asciidoc/management-interface-reference.adoc b/docs/src/main/asciidoc/management-interface-reference.adoc new file mode 100644 index 00000000000000..6d8093893d2fdf --- /dev/null +++ b/docs/src/main/asciidoc/management-interface-reference.adoc @@ -0,0 +1,163 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Management interface reference +include::_attributes.adoc[] +:categories: observability +:summary: Learn how you can use a separated management interface +:numbered: +:sectnums: +:sectnumlevels: 4 + +:numbered: +:sectnums: +:sectnumlevels: 4 + + +By default, Quarkus exposes the _management_ endpoints under `/q` on the main HTTP server. +The same HTTP server provides the application endpoints and the management endpoints. + +This document presents how you can use a separate HTTP server (bound to a different network interface and port) for the management endpoints. +It avoids exposing these endpoints on the main server and, therefore, prevents undesired accesses. + +== Enabling the management interface + +To enable the management interface, use the following **build-time** property: + +[source, properties] +---- +quarkus.management.enabled=true +---- + +Management endpoints will be exposed on: `http://0.0.0.0:9000/q`. +For example, `http://0.0.0.0:9000/q/health/ready` for the readiness probe. + +== Configure the host, port and scheme + +By default, the management interface is exposed on the interface: `0.0.0.0` (all interfaces) and on the port `9000` (`9001` in test mode). +It does not use TLS (`https`) by default. + +You can configure the host, ports, and TLS certificates using the following properties: + +* `quarkus.management.host` - the interface / host +* `quarkus.management.port` - the port +* `quarkus.management.test-port` - the port to use in test mode +* `quarkus.management.ssl` - the TLS configuration, xref:http-reference#ssl[same as for the main HTTP server]. + +Here is an example for properties exposing the management interface on _https://localhost:9002_: + +[source, properties] +---- +quarkus.management.enabled=true +quarkus.management.host=localhost +quarkus.management.port=9002 +quarkus.management.ssl.certificate.key-store-file=server-keystore.jks +quarkus.management.ssl.certificate.key-store-password=secret +---- + +IMPORTANT: Unlike the main HTTP server, the management interface does not handle _http_ and _https_ at the same time. +If _https_ is configured, plain HTTP requests will be rejected. + +== Configure the root path + +By default, management endpoints are exposed under `/q`. +This _root path_ can be configured using the `quarkus.management.root-path` property. +For example, if you want to expose the management endpoints under `/management` use: + +[source, properties] +---- +quarkus.management.enabled=true +quarkus.management.root-path=/management +---- + +The mounting rules of the management endpoints slightly differ from the ones used when using the main HTTP server: + +* Management endpoints configured using a _relative_ path (not starting with `/`) will be served from the configured root path. +For example, if the endpoint path is `health` and the root path is `management`, the resulting path is `/management/health` +* Management endpoints configured using an _absolute_ path (starting with `/`) will be served from the root. +For example, if the endpoint path is `/health`, the resulting path is `/health`, regardless of the root path +* The management interface does not use the HTTP root path from the main HTTP server. + +== Management endpoints + +SmallRye Health Checks, SmallRye Metrics and Prometheus are declared as management endpoints. + +NOTE: if you do not enable the management interface, these endpoints will be served using the main HTTP server (under `/q` by default). + +To declare a management endpoint, an extension must produce a _non application_ route and call the `management()` method: + +[source, java] +---- +@BuildStep +void createManagementRoute(BuildProducer routes, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + MyRecorder recorder) { + + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() // Must be called BEFORE the routeFunction method + .routeFunction("my-path", recorder.route()) + .handler(recorder.getHandler()) + .blockingRoute() + .build()); + //... +} +---- + +If the management interface is enabled, the endpoint will be exposed on: `http://0.0.0.0:9000/q/my-path`. +Otherwise, it will be exposed on: `http://localhost:8080/q/my-path`. + +IMPORTANT: Management endpoints can only be declared by extensions and not from the application code. + +== Management Interface Configuration + +include::{generated-dir}/config/quarkus-management-management-management-interface-build-time-config.adoc[leveloffset=+1, opts=optional] + +include::{generated-dir}/config/quarkus-management-management-management-interface-configuration.adoc[leveloffset=+1, opts=optional] + + +[[reverse-proxy]] +== Running behind a reverse proxy + +Quarkus could be accessed through proxies that additionally generate headers (e.g. `X-Forwarded-Host`) to keep +information from the client-facing side of the proxy servers that is altered or lost when they are involved. +In those scenarios, Quarkus can be configured to automatically update information like protocol, host, port and URI +reflecting the values in these headers. + +IMPORTANT: Activating this feature leaves the server exposed to several security issues (i.e. information spoofing). +Consider activate it only when running behind a reverse proxy. + +To set up this feature for the management interface, include the following lines in `src/main/resources/application.properties`: +[source,properties] +---- +quarkus.management.proxy.proxy-address-forwarding=true +---- + +To consider only de-facto standard header (`Forwarded` header), please include the following lines in `src/main/resources/application.properties`: +[source,properties] +---- +quarkus.management.proxy.allow-forwarded=true +---- + +To consider only non-standard headers, please include the following lines instead in `src/main/resources/application.properties`: + +[source,properties] +---- +quarkus.management.proxy.proxy-address-forwarding=true +quarkus.management.proxy.allow-x-forwarded=true +quarkus.management.proxy.enable-forwarded-host=true +quarkus.management.proxy.enable-forwarded-prefix=true +---- + +Both configurations related to standard and non-standard headers can be combined, although the standard headers configuration will have precedence. However, combining them has security implications as clients can forge requests with a forwarded header that is not overwritten by the proxy. +Therefore, proxies should strip unexpected `X-Forwarded` or `X-Forwarded-*` headers from the client. + +Supported forwarding address headers are: + +* `Forwarded` +* `X-Forwarded-Proto` +* `X-Forwarded-Host` +* `X-Forwarded-Port` +* `X-Forwarded-Ssl` +* `X-Forwarded-Prefix` diff --git a/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java b/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java index 47cba6debd7baf..cbe7063501d1eb 100644 --- a/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java +++ b/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java @@ -127,4 +127,4 @@ public void postBuild(ContainerImageInfoBuildItem image, List createDecorators(ApplicationInfoBuildItem applic readinessPath, roles, roleBindings, customProjectRoot); } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/pom.xml b/extensions/kubernetes/vanilla/deployment/pom.xml index 8c1d9f4b0c8d7b..cb2668a662062c 100644 --- a/extensions/kubernetes/vanilla/deployment/pom.xml +++ b/extensions/kubernetes/vanilla/deployment/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-kubernetes-spi + + io.quarkus + quarkus-vertx-http-deployment-spi + io.quarkus quarkus-kubernetes-client-internal-deployment diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java index a31cb79ff4d746..4c1cc733b8c347 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java @@ -146,4 +146,4 @@ private static int getStablePortNumberInRange(String input, int min, int max) { throw new RuntimeException("Unable to generate stable port number from input string: '" + input + "'", e); } } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java index f5490ee7272240..f1fb179d40ca57 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java @@ -366,4 +366,4 @@ private static List createVolumeDecorators(Optional }); return result; } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 9f6838391ae96e..9d412658ffd258 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -740,4 +740,4 @@ private static Map verifyPorts(List ku } return result; } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index d73924830a8673..8889892c000314 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -360,4 +360,4 @@ void externalizeInitTasks( decorators); } } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index 682e806001a560..66d85cda605121 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -275,4 +275,4 @@ void externalizeInitTasks( } } -} +} \ No newline at end of file diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java index 10652896c07293..2b0c5ea71e8a4c 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java @@ -95,6 +95,7 @@ void createPrometheusRoute(BuildProducer routes, // Exact match for resources matched to the root path routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path, recorder.route()) .routeConfigKey("quarkus.micrometer.export.prometheus.path") .handler(recorder.getHandler()) @@ -104,6 +105,7 @@ void createPrometheusRoute(BuildProducer routes, // Match paths that begin with the deployment path routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path + (pConfig.path.endsWith("/") ? "*" : "/*"), recorder.route()) .handler(recorder.getHandler()) .blockingRoute() @@ -111,10 +113,12 @@ void createPrometheusRoute(BuildProducer routes, // Fallback paths (for non text/plain requests) routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path, recorder.fallbackRoute()) .handler(recorder.getFallbackHandler()) .build()); routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path + (pConfig.path.endsWith("/") ? "*" : "/*"), recorder.fallbackRoute()) .handler(recorder.getFallbackHandler()) .build()); diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java new file mode 100644 index 00000000000000..577a4236998063 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java @@ -0,0 +1,35 @@ +package io.quarkus.micrometer.deployment.export; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class PrometheusEnabledOnManagementInterfaceTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") + .overrideConfigKey("quarkus.micrometer.export.prometheus.enabled", "true") + .overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false") + .overrideConfigKey("quarkus.redis.devservices.enabled", "false") + .overrideConfigKey("quarkus.management.enabled", "true") + .withEmptyApplication(); + + @Test + public void metricsEndpoint() { + RestAssured.given() + .accept("application/json") + .get("http://0.0.0.0:9001/q/metrics") + .then() + .log().all() + .statusCode(406); + + RestAssured.given() + .get("http://0.0.0.0:9001/q/metrics") + .then() + .statusCode(200); + } +} diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java index 02d5eeb1003a4a..755c74b85db704 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java @@ -8,6 +8,7 @@ import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -19,7 +20,8 @@ void setup(@Observes Router router) { HttpConfiguration httpConfiguration = new HttpConfiguration(); ConfigInstantiator.handleObject(httpConfiguration); Handler bodyHandler = new VertxHttpRecorder(new HttpBuildTimeConfig(), - new RuntimeValue<>(httpConfiguration)) + new ManagementInterfaceBuildTimeConfig(), + new RuntimeValue<>(httpConfiguration), null) .createBodyHandler(); router.route().order(Integer.MIN_VALUE + 1).handler(new Handler() { @Override diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 3ae05debeed906..d523397b0e4ecc 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -1,7 +1,5 @@ package io.quarkus.smallrye.health.deployment; -import static io.quarkus.arc.processor.Annotations.getAnnotations; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -69,6 +67,7 @@ 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.management.ManagementInterfaceBuildTimeConfig; import io.smallrye.health.SmallRyeHealthReporter; import io.smallrye.health.api.HealthGroup; import io.smallrye.health.api.HealthGroups; @@ -197,6 +196,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the health handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .route(healthConfig.rootPath) .routeConfigKey("quarkus.smallrye-health.root-path") .handler(new SmallRyeHealthHandler()) @@ -206,6 +206,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the liveness handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) .displayOnNotFoundPage() @@ -214,6 +215,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the readiness handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) .displayOnNotFoundPage() @@ -235,6 +237,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the health group handlers routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) .displayOnNotFoundPage() @@ -243,6 +246,7 @@ public void defineHealthRoutes(BuildProducer routes, SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/*") .handler(handler) .displayOnNotFoundPage() @@ -251,6 +255,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the wellness handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) .displayOnNotFoundPage() @@ -259,6 +264,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the startup handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.startupPath) .handler(new SmallRyeStartupHandler()) .displayOnNotFoundPage() @@ -287,17 +293,17 @@ public void processSmallRyeHealthConfigValues(SmallRyeHealthConfig healthConfig, @BuildStep(onlyIf = OpenAPIIncluded.class) public void includeInOpenAPIEndpoint(BuildProducer openAPIProducer, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, Capabilities capabilities, SmallRyeHealthConfig healthConfig) { // Add to OpenAPI if OpenAPI is available - if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI)) { + if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI) && !managementInterfaceBuildTimeConfig.enabled) { String healthRootPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.rootPath); - HealthOpenAPIFilter filter = new HealthOpenAPIFilter(healthRootPath, - nonApplicationRootPathBuildItem.resolveNestedPath(healthRootPath, healthConfig.livenessPath), - nonApplicationRootPathBuildItem.resolveNestedPath(healthRootPath, healthConfig.readinessPath), - nonApplicationRootPathBuildItem.resolveNestedPath(healthRootPath, healthConfig.startupPath)); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthRootPath, healthConfig.livenessPath), + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthRootPath, healthConfig.readinessPath), + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthRootPath, healthConfig.startupPath)); openAPIProducer.produce(new AddToOpenAPIDefinitionBuildItem(filter)); } @@ -333,15 +339,19 @@ public void kubernetes(NonApplicationRootPathBuildItem nonApplicationRootPathBui BuildProducer readinessPathItemProducer, BuildProducer startupPathItemProducer) { + // TODO Not that simple here... we need the port and stuff. livenessPathItemProducer.produce( new KubernetesHealthLivenessPathBuildItem( - nonApplicationRootPathBuildItem.resolveNestedPath(healthConfig.rootPath, healthConfig.livenessPath))); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthConfig.rootPath, + healthConfig.livenessPath))); readinessPathItemProducer.produce( new KubernetesHealthReadinessPathBuildItem( - nonApplicationRootPathBuildItem.resolveNestedPath(healthConfig.rootPath, healthConfig.readinessPath))); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthConfig.rootPath, + healthConfig.readinessPath))); startupPathItemProducer.produce( new KubernetesHealthStartupPathBuildItem( - nonApplicationRootPathBuildItem.resolveNestedPath(healthConfig.rootPath, healthConfig.startupPath))); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthConfig.rootPath, + healthConfig.startupPath))); } @BuildStep @@ -353,6 +363,7 @@ ShutdownListenerBuildItem shutdownListener() { @BuildStep void registerUiExtension( NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, SmallRyeHealthConfig healthConfig, LaunchModeBuildItem launchModeBuildItem, BuildProducer webJarBuildProducer) { @@ -365,7 +376,8 @@ void registerUiExtension( Set.of("quarkus.smallrye-health.root-path-ui")); } - String healthPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.rootPath); + String healthPath = nonApplicationRootPathBuildItem.resolveManagementPath(healthConfig.rootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem); webJarBuildProducer.produce( WebJarBuildItem.builder().artifactKey(HEALTH_UI_WEBJAR_ARTIFACT_KEY) // @@ -413,6 +425,7 @@ void registerHealthUiHandler( Handler handler = recorder.uiHandler(result.getFinalDestination(), healthUiPath, result.getWebRootConfigurations(), runtimeConfig, shutdownContext); + // The health ui is not a management route. routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(healthConfig.ui.rootPath) .displayOnNotFoundPage("Health UI") diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java new file mode 100644 index 00000000000000..33f48e3d16cbbc --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java @@ -0,0 +1,57 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceDisabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "false"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("/q/health/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("/q/health/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java new file mode 100644 index 00000000000000..8a5a168ff3597b --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java @@ -0,0 +1,58 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "true"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("http://0.0.0.0:9001/q/health/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("http://0.0.0.0:9001/q/health/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java new file mode 100644 index 00000000000000..eac5c437bcc6e7 --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java @@ -0,0 +1,58 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "true") + .overrideConfigKey("quarkus.smallrye-health.root-path", "/sante"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("http://0.0.0.0:9001/sante/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("http://0.0.0.0:9001/sante/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java new file mode 100644 index 00000000000000..24143522a39bdf --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java @@ -0,0 +1,59 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceWithRelativeRootPathTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "true") + .overrideConfigKey("quarkus.management.root-path", "/management") + .overrideConfigKey("quarkus.smallrye-health.root-path", "sante"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("http://0.0.0.0:9001/management/sante/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("http://0.0.0.0:9001/management/sante/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java index a1d836621b22f4..f737bab5999dfc 100644 --- a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java +++ b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java @@ -162,11 +162,13 @@ void createRoute(BuildProducer routes, displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(metrics.path)); } routes.produce(frameworkRoot.routeBuilder() + .management() .route(metrics.path + (metrics.path.endsWith("/") ? "*" : "/*")) .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) .blockingRoute() .build()); routes.produce(frameworkRoot.routeBuilder() + .management() .route(metrics.path) .routeConfigKey("quarkus.smallrye-metrics.path") .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) diff --git a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java new file mode 100644 index 00000000000000..40099c3c444396 --- /dev/null +++ b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java @@ -0,0 +1,10 @@ +package io.quarkus.vertx.http.deployment.spi; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item indicating that the application uses a separate interface:port for the management endpoints such + * as metrics, health and prometheus. + */ +public final class UseManagementInterfaceBuildItem extends SimpleBuildItem { +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java index d1d74aa653d103..98ef3b6f76e6b5 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java @@ -210,7 +210,7 @@ public Builder routeConfigKey(String attributeName) { @Override public RouteBuildItem build() { - return new RouteBuildItem(this, routeType, routerType); + return new RouteBuildItem(this, routeType, routerType, isManagement); } @Override @@ -218,6 +218,12 @@ protected ConfiguredPathInfo getRouteConfigInfo() { return super.getRouteConfigInfo(); } + @Override + public Builder management() { + super.management(); + return this; + } + @Override protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { return super.getNotFoundEndpoint(); 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 43d82e0db017b8..c39040cd9cf7d1 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 @@ -4,17 +4,25 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.util.UriNormalizationUtil; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.devmode.console.ConfiguredPathInfo; import io.quarkus.vertx.http.runtime.HandlerType; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { + + // TODO Should be handle the management root path? + /** * Normalized of quarkus.http.root-path. * Must end in a slash @@ -26,6 +34,11 @@ public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { */ final URI nonApplicationRootPath; + /** + * Normalized from quarkus.management.root-path + */ + final URI managementRootPath; + /** * Non-Application root path is distinct from HTTP root path. */ @@ -33,12 +46,14 @@ public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { final boolean attachedToMainRouter; - public NonApplicationRootPathBuildItem(String httpRootPath, String nonApplicationRootPath) { + public NonApplicationRootPathBuildItem(String httpRootPath, String nonApplicationRootPath, String managementRootPath) { // Presume value always starts with a slash and is normalized this.httpRootPath = UriNormalizationUtil.toURI(httpRootPath, true); this.nonApplicationRootPath = UriNormalizationUtil.normalizeWithBase(this.httpRootPath, nonApplicationRootPath, true); + this.managementRootPath = managementRootPath == null ? null + : UriNormalizationUtil.toURI("/" + managementRootPath, true); this.dedicatedRouterRequired = !this.nonApplicationRootPath.getPath().equals(this.httpRootPath.getPath()); @@ -95,6 +110,18 @@ public String getNonApplicationRootPath() { return nonApplicationRootPath.getPath(); } + /** + * @return the normalized root path for the mangement endpoints. {@code getNonApplicationRootPath()} if the + * management interface is disabled. + */ + public String getManagementRootPath() { + if (managementRootPath != null) { + return managementRootPath.getPath(); + } else { + return getNonApplicationRootPath(); + } + } + /** * Resolve path into an absolute path. * If path is relative, it will be resolved against `quarkus.http.non-application-root-path`. @@ -130,8 +157,8 @@ public String getNonApplicationRootPath() { * * @param path Path to be resolved to an absolute path. * @return An absolute path not ending with a slash - * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) * @throws IllegalArgumentException if path is null or empty + * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) */ public String resolvePath(String path) { if (path == null || path.trim().isEmpty()) { @@ -140,13 +167,52 @@ public String resolvePath(String path) { return UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, false).getPath(); } + public String resolveManagementPath(String path, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem mode) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Specified path can not be empty"); + } + if (!managementInterfaceBuildTimeConfig.enabled) { + if (managementRootPath != null) { + return UriNormalizationUtil.normalizeWithBase(managementRootPath, path, false).getPath(); + } + return UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, false).getPath(); + } else { + // Best effort + String prefix = getManagementUrlPrefix(mode); + if (managementRootPath != null) { + return prefix + UriNormalizationUtil.normalizeWithBase(managementRootPath, path, false).getPath(); + } else { + return prefix + path; + } + } + } + + /** + * Best effort to deduce the URL prefix (scheme, host, port) of the management interface. + * + * @param mode the mode, influencing the default port + * @return the prefix + */ + public static String getManagementUrlPrefix(LaunchModeBuildItem mode) { + Config config = ConfigProvider.getConfig(); + var managementHost = config.getOptionalValue("quarkus.management.host", String.class).orElse("0.0.0.0"); + var managementPort = config.getOptionalValue("quarkus.management.port", Integer.class).orElse(9000); + if (mode.isTest()) { + managementPort = config.getOptionalValue("quarkus.management.test-port", Integer.class).orElse(9001); + } + var isHttps = isTLsConfigured(config); + + return (isHttps ? "https://" : "http://") + managementHost + ":" + managementPort; + } + /** * Resolve a base path and a sub-resource against the non-application root. * This will call resolvePath on the base path (to establish a fully-resolved, * absolute path), and then will resolve the subRoute against that resolved path. * This allows a configured subpath to be configured (consistently) * as an absolute URI. - * + *

* Given {@literal quarkus.http.root-path=/} and * {@literal quarkus.http.non-application-root-path="q"} *

    @@ -158,9 +224,9 @@ public String resolvePath(String path) { * * @param path Path to be resolved to an absolute path. * @return An absolute path not ending with a slash + * @throws IllegalArgumentException if path is null or empty * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) * @see #resolvePath(String) - * @throws IllegalArgumentException if path is null or empty */ public String resolveNestedPath(String path, String subRoute) { if (path == null || path.trim().isEmpty()) { @@ -170,6 +236,19 @@ public String resolveNestedPath(String path, String subRoute) { return UriNormalizationUtil.normalizeWithBase(base, subRoute, false).getPath(); } + public String resolveManagementNestedPath(String path, String subRoute) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Specified path can not be empty"); + } + URI base; + if (managementRootPath != null) { + base = UriNormalizationUtil.normalizeWithBase(managementRootPath, path, true); + } else { + base = UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, true); + } + return UriNormalizationUtil.normalizeWithBase(base, subRoute, false).getPath(); + } + public Builder routeBuilder() { return new Builder(this); } @@ -194,8 +273,21 @@ public Builder routeFunction(Function routeFunction) { } public Builder routeFunction(String route, Consumer routeFunction) { - route = super.absolutePath = buildItem.resolvePath(route); + 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. + if (route.startsWith("/")) { + this.path = route; + } else { + this.path = buildItem.getManagementRootPath() + route; + } + this.routerType = RouteBuildItem.RouteType.ABSOLUTE_ROUTE; + super.routeFunction(this.path, routeFunction); + return this; + } + + route = super.absolutePath = buildItem.resolvePath(route); boolean isFrameworkRoute = buildItem.dedicatedRouterRequired && route.startsWith(buildItem.getNonApplicationRootPath()); @@ -278,7 +370,7 @@ public Builder routeConfigKey(String attributeName) { @Override public RouteBuildItem build() { - return new RouteBuildItem(this, routeType, routerType); + return new RouteBuildItem(this, routeType, routerType, isManagement); } @Override @@ -288,7 +380,50 @@ protected ConfiguredPathInfo getRouteConfigInfo() { @Override protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { - return super.getNotFoundEndpoint(); + if (!displayOnNotFoundPage) { + return null; + } + if (isManagement && buildItem.managementRootPath != null) { + return null; // Exposed on the management interface, so not exposed. + } + if (notFoundPagePath == null) { + throw new RuntimeException("Cannot display " + routeFunction + + " on not found page as no explicit path was specified and a route function is in use"); + } + if (absolutePath != null) { + return new NotFoundPageDisplayableEndpointBuildItem(absolutePath, notFoundPageTitle, true); + } + return new NotFoundPageDisplayableEndpointBuildItem(notFoundPagePath, notFoundPageTitle, false); + } + + @Override + public Builder management() { + super.management(); + return this; } } + + /** + * Best effort to check if the management interface is using {@code https}. + * + * @param config the config + * @return {@code true} if the management interface configuration contains a key or a certificate (indicating TLS) + */ + private static boolean isTLsConfigured(Config config) { + var hasCert = config.getOptionalValue("quarkus.management.ssl.certificate.file", String.class).isPresent(); + var hasKey = config.getOptionalValue("quarkus.management.ssl.certificate.key-file", String.class).isPresent(); + + var hasKeys = config.getOptionalValue("quarkus.management.ssl.certificate.key-files", String.class).isPresent(); + var hasCerts = config.getOptionalValue("quarkus.management.ssl.certificate.files", String.class).isPresent(); + + var hasProvider = config.getOptionalValue("quarkus.management.ssl.certificate.credential-provider", String.class) + .isPresent(); + var hasProviderName = config + .getOptionalValue("quarkus.management.ssl.certificate.credential-provider-name", String.class).isPresent(); + + var hasKeyStore = config.getOptionalValue("quarkus.management.ssl.certificate.key-store-file", String.class) + .isPresent(); + + return hasCerts || hasKeys || hasCert || hasKey || hasProvider || hasProviderName || hasKeyStore; + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java index 4f75d3b374a220..f4f985baee2469 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java @@ -21,6 +21,8 @@ public static Builder builder() { return new Builder(); } + private final boolean management; + private final Function routeFunction; private final Handler handler; private final HandlerType type; @@ -29,9 +31,10 @@ public static Builder builder() { private final NotFoundPageDisplayableEndpointBuildItem notFoundPageDisplayableEndpoint; private final ConfiguredPathInfo configuredPathInfo; - RouteBuildItem(Builder builder, RouteType routeType, RouteType routerType) { + RouteBuildItem(Builder builder, RouteType routeType, RouteType routerType, boolean management) { this.routeFunction = builder.routeFunction; this.handler = builder.handler; + this.management = management; this.type = builder.type; this.routeType = routeType; this.routerType = routerType; @@ -79,6 +82,15 @@ public ConfiguredPathInfo getConfiguredPathInfo() { return configuredPathInfo; } + /** + * @return {@code true} if the route is exposing a management endpoint. + * It matters when using a different interface/port for the management endpoints, as these routes will only + * be accessible from that different interface/port. + */ + public boolean isManagement() { + return management; + } + public enum RouteType { FRAMEWORK_ROUTE, APPLICATION_ROUTE, @@ -100,6 +112,8 @@ public static class Builder { protected String routeConfigKey; protected String absolutePath; + protected boolean isManagement; + /** * {@link #routeFunction(String, Consumer)} should be used instead * @@ -195,12 +209,17 @@ public Builder routeConfigKey(String attributeName) { return this; } + public Builder management() { + this.isManagement = true; + return this; + } + public RouteBuildItem build() { if (routeFunction == null) { throw new IllegalStateException( "'RouteBuildItem$Builder.routeFunction' was not set. Ensure that one of the builder methods that result in it being set is called"); } - return new RouteBuildItem(this, APPLICATION_ROUTE, APPLICATION_ROUTE); + return new RouteBuildItem(this, APPLICATION_ROUTE, APPLICATION_ROUTE, isManagement); } protected ConfiguredPathInfo getRouteConfigInfo() { @@ -212,9 +231,9 @@ protected ConfiguredPathInfo getRouteConfigInfo() { + " as no explicit path was specified and a route function is in use"); } if (absolutePath != null) { - return new ConfiguredPathInfo(routeConfigKey, absolutePath, true); + return new ConfiguredPathInfo(routeConfigKey, absolutePath, true, isManagement); } - return new ConfiguredPathInfo(routeConfigKey, routePath, false); + return new ConfiguredPathInfo(routeConfigKey, routePath, false, isManagement); } protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { 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 14faf8cd2b9a85..178c50e731152c 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 @@ -51,6 +51,7 @@ import io.quarkus.vertx.http.deployment.devmode.HttpRemoteDevClientProvider; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem; +import io.quarkus.vertx.http.deployment.spi.UseManagementInterfaceBuildItem; import io.quarkus.vertx.http.runtime.BasicRoute; import io.quarkus.vertx.http.runtime.CurrentRequestProducer; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; @@ -62,6 +63,7 @@ import io.quarkus.vertx.http.runtime.cors.CORSRecorder; import io.quarkus.vertx.http.runtime.filters.Filter; import io.quarkus.vertx.http.runtime.filters.GracefulShutdownFilter; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.core.http.impl.Http1xServerRequest; import io.vertx.core.impl.VertxImpl; @@ -86,18 +88,26 @@ HttpRootPathBuildItem httpRoot(HttpBuildTimeConfig httpBuildTimeConfig) { } @BuildStep - NonApplicationRootPathBuildItem frameworkRoot(HttpBuildTimeConfig httpBuildTimeConfig) { - return new NonApplicationRootPathBuildItem(httpBuildTimeConfig.rootPath, httpBuildTimeConfig.nonApplicationRootPath); + NonApplicationRootPathBuildItem frameworkRoot(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig) { + String mrp = null; + if (managementBuildTimeConfig.enabled) { + mrp = managementBuildTimeConfig.rootPath; + } + return new NonApplicationRootPathBuildItem(httpBuildTimeConfig.rootPath, httpBuildTimeConfig.nonApplicationRootPath, + mrp); } @BuildStep FrameworkEndpointsBuildItem frameworkEndpoints(NonApplicationRootPathBuildItem nonApplicationRootPath, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, LaunchModeBuildItem launchModeBuildItem, List routes) { List frameworkEndpoints = new ArrayList<>(); for (RouteBuildItem route : routes) { if (FRAMEWORK_ROUTE.equals(route.getRouteType())) { if (route.getConfiguredPathInfo() != null) { - frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath)); + frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem)); continue; } if (route.getRouteFunction() != null && route.getRouteFunction() instanceof BasicRoute) { @@ -133,6 +143,14 @@ UnremovableBeanBuildItem shouldNotRemoveHttpServerOptionsCustomizers() { return UnremovableBeanBuildItem.beanTypes(HttpServerOptionsCustomizer.class); } + @BuildStep + UseManagementInterfaceBuildItem useManagementInterfaceBuildItem(ManagementInterfaceBuildTimeConfig config) { + if (config.enabled) { + return new UseManagementInterfaceBuildItem(); + } + return null; + } + /** * Workaround for https://github.com/quarkusio/quarkus/issues/4720 by filtering Vertx multiple instance warning in dev * mode. @@ -163,6 +181,16 @@ public void kubernetes(BuildProducer kubernetesPorts) { kubernetesPorts.produce(new KubernetesPortBuildItem(port, "http")); } + @BuildStep + public KubernetesPortBuildItem kubernetesForManagement( + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + if (managementInterfaceBuildTimeConfig.enabled) { + int port = ConfigProvider.getConfig().getOptionalValue("quarkus.management.port", Integer.class).orElse(9000); + return new KubernetesPortBuildItem(port, "management"); + } + return null; + } + @BuildStep void notFoundRoutes( List routes, @@ -201,6 +229,7 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, CoreVertxBuildItem vertx, List routes, HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, NonApplicationRootPathBuildItem nonApplicationRootPath, ShutdownContextBuildItem shutdown) { @@ -208,19 +237,28 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, RuntimeValue mutinyRouter = initialRouter.getMutinyRouter(); RuntimeValue frameworkRouter = null; RuntimeValue mainRouter = null; + RuntimeValue managementRouter = null; List redirectRoutes = new ArrayList<>(); boolean frameworkRouterCreated = false; boolean mainRouterCreated = false; + boolean managementRouterCreated = false; + + boolean isManagementInterfaceEnabled = managementBuildTimeConfig.enabled; for (RouteBuildItem route : routes) { - if (nonApplicationRootPath.isDedicatedRouterRequired() && route.isRouterFramework()) { + if (route.isManagement() && isManagementInterfaceEnabled) { + if (!managementRouterCreated) { + managementRouter = recorder.initializeRouter(vertx.getVertx()); + managementRouterCreated = true; + } + recorder.addRoute(managementRouter, route.getRouteFunction(), route.getHandler(), route.getType()); + } else if (nonApplicationRootPath.isDedicatedRouterRequired() && route.isRouterFramework()) { // Non-application endpoints on a separate path if (!frameworkRouterCreated) { frameworkRouter = recorder.initializeRouter(vertx.getVertx()); frameworkRouterCreated = true; } - recorder.addRoute(frameworkRouter, route.getRouteFunction(), route.getHandler(), route.getType()); } else if (route.isRouterAbsolute()) { // Add Route to "/" @@ -246,7 +284,7 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, } } - return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter, frameworkRouter, mutinyRouter); + return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter, frameworkRouter, managementRouter, mutinyRouter); } @BuildStep @@ -323,6 +361,7 @@ ServiceStartBuildItem finalizeRouter( defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, vertx.getVertx(), lrc, mainRouter, httpRouteRouter.getHttpRouter(), httpRouteRouter.getMutinyRouter(), httpRouteRouter.getFrameworkRouter(), + httpRouteRouter.getManagementRouter(), httpRootPathBuildItem.getRootPath(), nonApplicationRootPathBuildItem.getNonApplicationRootPath(), launchMode.getLaunchMode(), diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java index 391a48592fdeed..f66cd2154c48a0 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java @@ -10,12 +10,16 @@ public final class VertxWebRouterBuildItem extends SimpleBuildItem { private final RuntimeValue mainRouter; private final RuntimeValue frameworkRouter; private final RuntimeValue mutinyRouter; + private final RuntimeValue managementRouter; VertxWebRouterBuildItem(RuntimeValue httpRouter, RuntimeValue mainRouter, - RuntimeValue frameworkRouter, RuntimeValue mutinyRouter) { + RuntimeValue frameworkRouter, + RuntimeValue managementRouter, + RuntimeValue mutinyRouter) { this.httpRouter = httpRouter; this.mainRouter = mainRouter; this.frameworkRouter = frameworkRouter; + this.managementRouter = managementRouter; // Can be null if the management interface is disabled this.mutinyRouter = mutinyRouter; } @@ -45,4 +49,13 @@ RuntimeValue getMainRouter() { RuntimeValue getFrameworkRouter() { return frameworkRouter; } + + /** + * Will be {@code null} if the management interface is disabled. + * + * @return RuntimeValue + */ + RuntimeValue getManagementRouter() { + return managementRouter; + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java index a522178d15ab9c..cea49d8df6854a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java @@ -1,18 +1,22 @@ package io.quarkus.vertx.http.deployment.devmode.console; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.runtime.TemplateHtmlBuilder; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; public class ConfiguredPathInfo { private final String name; private final String endpointPath; private final boolean absolutePath; + private final boolean management; - public ConfiguredPathInfo(String name, String endpointPath, boolean isAbsolutePath) { + public ConfiguredPathInfo(String name, String endpointPath, boolean isAbsolutePath, boolean management) { this.name = name; this.endpointPath = endpointPath; this.absolutePath = isAbsolutePath; + this.management = management; } public String getName() { @@ -27,7 +31,12 @@ public String getEndpointPath(HttpRootPathBuildItem httpRoot) { } } - public String getEndpointPath(NonApplicationRootPathBuildItem nonAppRoot) { + public String getEndpointPath(NonApplicationRootPathBuildItem nonAppRoot, ManagementInterfaceBuildTimeConfig mibt, + LaunchModeBuildItem mode) { + if (management && mibt.enabled) { + var prefix = NonApplicationRootPathBuildItem.getManagementUrlPrefix(mode); + return prefix + endpointPath; + } if (absolutePath) { return endpointPath; } else { 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 5afe466d5054a5..fec79b47321cfa 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 @@ -102,6 +102,7 @@ import io.quarkus.vertx.http.runtime.devmode.RuntimeDevConsoleRoute; import io.quarkus.vertx.http.runtime.logstream.LogStreamRecorder; import io.quarkus.vertx.http.runtime.logstream.WebSocketLogHandler; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.smallrye.common.vertx.VertxContext; import io.smallrye.config.common.utils.StringUtil; import io.vertx.core.Handler; @@ -340,6 +341,7 @@ public ServiceStartBuildItem setupDeploymentSideHandling(List allRoutes, List routes, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, List configDescriptionBuildItems, LaunchModeBuildItem launchModeBuildItem) { if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { @@ -352,6 +354,7 @@ public ServiceStartBuildItem setupDeploymentSideHandling(List devTemplatePaths, BuildSystemTargetBuildItem buildSystemTargetBuildItem, Optional effectiveIdeBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, List configDescriptionBuildItems, LaunchModeBuildItem launchModeBuildItem) { EngineBuilder builder = Engine.builder().addDefaults(); @@ -618,8 +622,9 @@ private Engine buildEngine(List devTemplatePaths, for (RouteBuildItem item : allRoutes) { ConfiguredPathInfo resolvedPathBuildItem = item.getConfiguredPathInfo(); if (resolvedPathBuildItem != null) { - resolvedPaths.put(resolvedPathBuildItem.getName(), - resolvedPathBuildItem.getEndpointPath(nonApplicationRootPathBuildItem)); + String path = resolvedPathBuildItem.getEndpointPath(nonApplicationRootPathBuildItem, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + resolvedPaths.put(resolvedPathBuildItem.getName(), path); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java index 243bcca9b71c9c..36f3fac9e9128d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java @@ -64,7 +64,8 @@ public void handle(RoutingContext routingContext) { @Test public void testNonApplicationEndpointDirect() { // Note RestAssured knows the path prefix is /api - RestAssured.given().get("/q/non-app-relative").then().statusCode(200).body(Matchers.equalTo("/api/q/non-app-relative")); + RestAssured.given().get("/q/non-app-relative") + .then().statusCode(200).body(Matchers.equalTo("/api/q/non-app-relative")); } @Singleton diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java index 4844ba60a7ba84..90796a00e584c0 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java @@ -1,12 +1,19 @@ package io.quarkus.vertx.http.deployment; +import java.util.Optional; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; + public class NonApplicationRootPathBuildItemTest { + @Test void testResolvePathWithSlashRelativeQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -24,7 +31,7 @@ void testResolvePathWithSlashRelativeQ() { @Test void testResolvePathWithSlashAbsoluteQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -38,7 +45,7 @@ void testResolvePathWithSlashAbsoluteQ() { @Test void testResolvePathWithSlashAppWithRelativeQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -52,7 +59,7 @@ void testResolvePathWithSlashAppWithRelativeQ() { @Test void testResolvePathWithSlashAppWithAbsoluteQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "/q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "/q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertFalse(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -66,7 +73,7 @@ void testResolvePathWithSlashAppWithAbsoluteQ() { @Test void testResolvePathWithSlashEmpty() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", ""); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "", null); Assertions.assertFalse(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/", buildItem.getVertxRouterPath()); @@ -80,7 +87,7 @@ void testResolvePathWithSlashEmpty() { @Test void testResolvePathWithSlashWithSlash() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/", null); Assertions.assertFalse(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/", buildItem.getVertxRouterPath()); @@ -94,8 +101,155 @@ void testResolvePathWithSlashWithSlash() { @Test void testResolvePathWithSlashWithSlashQWithWildcards() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", null); Assertions.assertEquals("/q/foo/*", buildItem.resolvePath("foo/*")); Assertions.assertEquals("/foo/*", buildItem.resolvePath("/foo/*")); } + + @Test + void testResolveManagementPathWithRelativeRootPath() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/management/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithRelativeRootPathInTestMode() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), true); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/management/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9001/management/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9001/management/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9001/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9001/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithRelativeRootPathAndWithManagementDisabled() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = false; + managementInterfaceBuildTimeConfig.rootPath = "management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", null); + + Assertions.assertEquals("/q/", buildItem.getManagementRootPath()); + Assertions.assertEquals("/q/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("/q/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithAbsoluteRootPath() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "/management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/management/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithEmptyRootPath() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = ""; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithWithWildcards() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "/management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo/*", + buildItem.resolveManagementPath("foo/*", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/*", + buildItem.resolveManagementPath("/foo/*", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java index 116db1c8a43f16..7d355f2aa9a2d9 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java @@ -8,6 +8,7 @@ import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -19,7 +20,8 @@ void setup(@Observes Router router) { HttpConfiguration httpConfiguration = new HttpConfiguration(); ConfigInstantiator.handleObject(httpConfiguration); Handler bodyHandler = new VertxHttpRecorder(new HttpBuildTimeConfig(), - new RuntimeValue<>(httpConfiguration)) + new ManagementInterfaceBuildTimeConfig(), + new RuntimeValue<>(httpConfiguration), null) .createBodyHandler(); router.route().order(Integer.MIN_VALUE + 1).handler(new Handler() { @Override diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java new file mode 100644 index 00000000000000..1e9fe401909c96 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.management; + +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementAndRootPathTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("management-relative") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + routingContext.response() + .setStatusCode(200) + .end(routingContext.request().path()); + } + } + + @Test + public void testNonApplicationEndpointDirect() { + // Note RestAssured knows the path prefix is /api + RestAssured.given().get("http://0.0.0.0:9001/management/management-relative") + .then().statusCode(200).body(Matchers.equalTo("/management/management-relative")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java new file mode 100644 index 00000000000000..580fd44b270660 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java @@ -0,0 +1,89 @@ +package io.quarkus.vertx.http.management; + +import java.io.File; +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithJksTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.management.ssl.certificate.key-store-file=server-keystore.jks\n" + + "quarkus.management.ssl.certificate.key-store-password=secret\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addAsResource(new File("src/test/resources/conf/server-keystore.jks"), "server-keystore.jks") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + rc.response().end("ssl"); + } + } + + @Test + public void testSslWithJks() { + RestAssured.given() + .relaxedHTTPSValidation() + .get("https://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ssl")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java new file mode 100644 index 00000000000000..9694283a305a94 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java @@ -0,0 +1,81 @@ +package io.quarkus.vertx.http.management; + +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithMainServerDisabledTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.http.host-enabled=false\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + rc.response().end("ok"); + } + } + + @Test + public void testManagementWithoutMain() { + RestAssured.given() + .get("http://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ok")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java new file mode 100644 index 00000000000000..0f730779f08564 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java @@ -0,0 +1,89 @@ +package io.quarkus.vertx.http.management; + +import java.io.File; +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithP12Test { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.management.ssl.certificate.key-store-file=server-keystore.p12\n" + + "quarkus.management.ssl.certificate.key-store-password=secret\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addAsResource(new File("src/test/resources/conf/server-keystore.p12"), "server-keystore.p12") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + rc.response().end("ssl"); + } + } + + @Test + public void testSslWithP12() { + RestAssured.given() + .relaxedHTTPSValidation() + .get("https://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ssl")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java new file mode 100644 index 00000000000000..ef4b24d1638a14 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java @@ -0,0 +1,90 @@ +package io.quarkus.vertx.http.management; + +import java.io.File; +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithPemTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.management.ssl.certificate.files=server-cert.pem\n" + + "quarkus.management.ssl.certificate.key-files=server-key.pem\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addAsResource(new File("src/test/resources/conf/server-key.pem"), "server-key.pem") + .addAsResource(new File("src/test/resources/conf/server-cert.pem"), "server-cert.pem") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + rc.response().end("ssl"); + } + } + + @Test + public void testSslWithPem() { + RestAssured.given() + .relaxedHTTPSValidation() + .get("https://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ssl")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java index 19ae4a6270a265..6ba0cb9d1cc077 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java @@ -22,7 +22,7 @@ * Restricts who can send `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers to trusted proxies * configured through {@link ProxyConfig#trustedProxies}. */ -class ForwardedProxyHandler implements Handler { +public class ForwardedProxyHandler implements Handler { private static final Logger LOGGER = Logger.getLogger(ForwardedProxyHandler.class.getName()); @@ -34,7 +34,7 @@ class ForwardedProxyHandler implements Handler { private final ForwardingProxyOptions forwardingProxyOptions; - ForwardedProxyHandler(TrustedProxyCheck.TrustedProxyCheckBuilder proxyCheckBuilder, + public ForwardedProxyHandler(TrustedProxyCheck.TrustedProxyCheckBuilder proxyCheckBuilder, Supplier vertx, Handler delegate, ForwardingProxyOptions forwardingProxyOptions) { this.proxyCheckBuilder = proxyCheckBuilder; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java index 7e93a5a193aadb..d4da4d5c7f44d0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java @@ -29,7 +29,7 @@ import io.vertx.core.net.NetSocket; import io.vertx.core.net.SocketAddress; -class ForwardedServerRequestWrapper extends HttpServerRequestWrapper implements HttpServerRequest { +public class ForwardedServerRequestWrapper extends HttpServerRequestWrapper implements HttpServerRequest { private final ForwardedParser forwardedParser; private boolean modified; @@ -40,7 +40,7 @@ class ForwardedServerRequestWrapper extends HttpServerRequestWrapper implements private String uri; private String absoluteURI; - ForwardedServerRequestWrapper(HttpServerRequest request, ForwardingProxyOptions forwardingProxyOptions, + public ForwardedServerRequestWrapper(HttpServerRequest request, ForwardingProxyOptions forwardingProxyOptions, TrustedProxyCheck trustedProxyCheck) { super((HttpServerRequestInternal) request); forwardedParser = new ForwardedParser(delegate, forwardingProxyOptions, trustedProxyCheck); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java index be0e6604859d89..e7c0cf032a6fbc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java @@ -7,14 +7,14 @@ import io.quarkus.vertx.http.runtime.TrustedProxyCheck.TrustedProxyCheckPart; public class ForwardingProxyOptions { - final boolean proxyAddressForwarding; + public final boolean proxyAddressForwarding; final boolean allowForwarded; final boolean allowXForwarded; final boolean enableForwardedHost; final boolean enableForwardedPrefix; final AsciiString forwardedHostHeader; final AsciiString forwardedPrefixHeader; - final TrustedProxyCheckBuilder trustedProxyCheckBuilder; + public final TrustedProxyCheckBuilder trustedProxyCheckBuilder; public ForwardingProxyOptions(final boolean proxyAddressForwarding, final boolean allowForwarded, @@ -34,18 +34,18 @@ public ForwardingProxyOptions(final boolean proxyAddressForwarding, this.trustedProxyCheckBuilder = trustedProxyCheckBuilder; } - public static ForwardingProxyOptions from(HttpConfiguration httpConfiguration) { - final boolean proxyAddressForwarding = httpConfiguration.proxy.proxyAddressForwarding; - final boolean allowForwarded = httpConfiguration.proxy.allowForwarded; - final boolean allowXForwarded = httpConfiguration.proxy.allowXForwarded.orElse(!allowForwarded); + public static ForwardingProxyOptions from(ProxyConfig proxy) { + final boolean proxyAddressForwarding = proxy.proxyAddressForwarding; + final boolean allowForwarded = proxy.allowForwarded; + final boolean allowXForwarded = proxy.allowXForwarded.orElse(!allowForwarded); - final boolean enableForwardedHost = httpConfiguration.proxy.enableForwardedHost; - final boolean enableForwardedPrefix = httpConfiguration.proxy.enableForwardedPrefix; - final AsciiString forwardedPrefixHeader = AsciiString.cached(httpConfiguration.proxy.forwardedPrefixHeader); - final AsciiString forwardedHostHeader = AsciiString.cached(httpConfiguration.proxy.forwardedHostHeader); + final boolean enableForwardedHost = proxy.enableForwardedHost; + final boolean enableForwardedPrefix = proxy.enableForwardedPrefix; + final AsciiString forwardedPrefixHeader = AsciiString.cached(proxy.forwardedPrefixHeader); + final AsciiString forwardedHostHeader = AsciiString.cached(proxy.forwardedHostHeader); - final List parts = httpConfiguration.proxy.trustedProxies - .isPresent() ? List.copyOf(httpConfiguration.proxy.trustedProxies.get()) : List.of(); + final List parts = proxy.trustedProxies + .isPresent() ? List.copyOf(proxy.trustedProxies.get()) : List.of(); final var proxyCheckBuilder = (!allowXForwarded && !allowForwarded) || parts.isEmpty() ? null : TrustedProxyCheckBuilder.builder(parts); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java index f30dbe982d0584..4375618f3201dc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java @@ -7,11 +7,11 @@ import io.vertx.core.http.impl.HttpServerRequestInternal; import io.vertx.core.http.impl.HttpServerRequestWrapper; -class ResumingRequestWrapper extends HttpServerRequestWrapper { +public class ResumingRequestWrapper extends HttpServerRequestWrapper { private boolean userSetState; - ResumingRequestWrapper(HttpServerRequest request) { + public ResumingRequestWrapper(HttpServerRequest request) { super((HttpServerRequestInternal) request); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java index 1c33d410a3accd..2fa0f75f678df0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java @@ -10,7 +10,7 @@ import io.vertx.core.net.SocketAddress; -interface TrustedProxyCheck { +public interface TrustedProxyCheck { static TrustedProxyCheck allowAll() { return new TrustedProxyCheck() { 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 9b4415ce8b7d51..4a7d012ee72724 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 @@ -1,23 +1,17 @@ package io.quarkus.vertx.http.runtime; import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; -import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setCurrentContextSafe; -import static io.quarkus.vertx.http.runtime.TrustedProxyCheck.allowAll; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.BindException; import java.net.URI; import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -26,7 +20,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -50,8 +43,6 @@ import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.bootstrap.runner.Timing; -import io.quarkus.credentials.CredentialsProvider; -import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.spi.HotReplacementContext; import io.quarkus.netty.runtime.virtual.VirtualAddress; @@ -64,15 +55,12 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.runtime.shutdown.ShutdownConfig; -import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.core.runtime.VertxCoreRecorder; import io.quarkus.vertx.core.runtime.config.VertxConfiguration; import io.quarkus.vertx.http.HttpServerOptionsCustomizer; import io.quarkus.vertx.http.runtime.HttpConfiguration.InsecureRequests; -import io.quarkus.vertx.http.runtime.TrustedProxyCheck.TrustedProxyCheckBuilder; import io.quarkus.vertx.http.runtime.devmode.RemoteSyncHandler; import io.quarkus.vertx.http.runtime.devmode.VertxHttpHotReplacementSetup; import io.quarkus.vertx.http.runtime.filters.Filter; @@ -83,6 +71,10 @@ import io.quarkus.vertx.http.runtime.filters.accesslog.AccessLogReceiver; import io.quarkus.vertx.http.runtime.filters.accesslog.DefaultAccessLogReceiver; import io.quarkus.vertx.http.runtime.filters.accesslog.JBossLoggingAccessLogReceiver; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; +import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers; +import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils; import io.smallrye.common.vertx.VertxContext; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; @@ -92,7 +84,6 @@ import io.vertx.core.Promise; import io.vertx.core.Verticle; import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.Cookie; import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpConnection; @@ -101,15 +92,11 @@ import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpVersion; import io.vertx.core.http.impl.Http1xServerConnection; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.EventLoopContext; import io.vertx.core.impl.Utils; import io.vertx.core.impl.VertxInternal; -import io.vertx.core.net.JdkSSLEngineOptions; -import io.vertx.core.net.KeyStoreOptions; -import io.vertx.core.net.PemKeyCertOptions; import io.vertx.core.net.SocketAddress; import io.vertx.core.net.impl.ConnectionBase; import io.vertx.core.net.impl.VertxHandler; @@ -117,6 +104,7 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.CorsHandler; @Recorder public class VertxHttpRecorder { @@ -158,6 +146,8 @@ public class VertxHttpRecorder { private static volatile int actualHttpPort = -1; private static volatile int actualHttpsPort = -1; + private static volatile int actualManagementPort = -1; + public static final String GET = "GET"; private static final Handler ACTUAL_ROOT = new Handler() { @@ -205,12 +195,25 @@ private boolean uriValid(HttpServerRequest httpServerRequest) { } } }; + private static HttpServerOptions httpMainSslServerOptions; + private static HttpServerOptions httpMainServerOptions; + private static HttpServerOptions httpMainDomainSocketOptions; + private static HttpServerOptions httpManagementServerOptions; final HttpBuildTimeConfig httpBuildTimeConfig; + final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig; final RuntimeValue httpConfiguration; - public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, RuntimeValue httpConfiguration) { + final RuntimeValue managementConfiguration; + private static volatile Handler managementRouter; + + public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, + RuntimeValue httpConfiguration, + RuntimeValue managementConfiguration) { this.httpBuildTimeConfig = httpBuildTimeConfig; this.httpConfiguration = httpConfiguration; + this.managementBuildTimeConfig = managementBuildTimeConfig; + this.managementConfiguration = managementConfiguration; } public static void setHotReplacement(Handler handler, HotReplacementContext hrc) { @@ -252,8 +255,12 @@ public static void startServerAfterFailedStart() { try { HttpBuildTimeConfig buildConfig = new HttpBuildTimeConfig(); ConfigInstantiator.handleObject(buildConfig); + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + ConfigInstantiator.handleObject(managementBuildTimeConfig); HttpConfiguration config = new HttpConfiguration(); ConfigInstantiator.handleObject(config); + ManagementInterfaceConfiguration managementConfig = new ManagementInterfaceConfiguration(); + ConfigInstantiator.handleObject(managementConfig); if (config.host == null) { //HttpHostConfigSource does not come into play here config.host = "localhost"; @@ -273,12 +280,13 @@ public static void startServerAfterFailedStart() { rootHandler = root; //we can't really do - doServerStart(vertx, buildConfig, config, LaunchMode.DEVELOPMENT, new Supplier() { - @Override - public Integer get() { - return ProcessorInfo.availableProcessors(); //this is dev mode, so the number of IO threads not always being 100% correct does not really matter in this case - } - }, null, false); + doServerStart(vertx, buildConfig, managementBuildTimeConfig, null, config, managementConfig, LaunchMode.DEVELOPMENT, + new Supplier() { + @Override + public Integer get() { + return ProcessorInfo.availableProcessors(); //this is dev mode, so the number of IO threads not always being 100% correct does not really matter in this case + } + }, null, false); } catch (Exception e) { throw new RuntimeException(e); } @@ -314,11 +322,15 @@ public void startServer(Supplier vertx, ShutdownContext shutdown, }); } HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); - if (startSocket && (httpConfiguration.hostEnabled || httpConfiguration.domainSocketEnabled)) { + ManagementInterfaceConfiguration managementConfig = this.managementConfiguration == null ? null + : this.managementConfiguration.getValue(); + if (startSocket && (httpConfiguration.hostEnabled || httpConfiguration.domainSocketEnabled + || managementConfig.hostEnabled || managementConfig.domainSocketEnabled)) { // Start the server if (closeTask == null) { - doServerStart(vertx.get(), httpBuildTimeConfig, httpConfiguration, launchMode, ioThreads, - websocketSubProtocols, auxiliaryApplication); + doServerStart(vertx.get(), httpBuildTimeConfig, managementBuildTimeConfig, managementRouter, + httpConfiguration, managementConfig, launchMode, ioThreads, websocketSubProtocols, + auxiliaryApplication); if (launchMode != LaunchMode.DEVELOPMENT) { shutdown.addShutdownTask(closeTask); } else { @@ -342,7 +354,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute List filterList, Supplier vertx, LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue, RuntimeValue httpRouterRuntimeValue, RuntimeValue mutinyRouter, - RuntimeValue frameworkRouter, + RuntimeValue frameworkRouter, RuntimeValue managementRouter, String rootPath, String nonRootPath, LaunchMode launchMode, boolean requireBodyHandler, Handler bodyHandler, @@ -382,18 +394,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute defaultRouteHandler.accept(httpRouteRouter.route().order(DEFAULT_ROUTE_ORDER)); } - if (httpBuildTimeConfig.enableCompression) { - httpRouteRouter.route().order(0).handler(new Handler() { - @Override - public void handle(RoutingContext ctx) { - // Add "Content-Encoding: identity" header that disables the compression - // This header can be removed to enable the compression - ctx.response().putHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY); - ctx.next(); - } - }); - } - + applyCompression(httpBuildTimeConfig.enableCompression, httpRouteRouter); httpRouteRouter.route().last().failureHandler( new QuarkusErrorHandler(launchMode.isDevOrTest(), httpConfiguration.unhandledErrorContentTypeDefault)); @@ -409,101 +410,12 @@ public void handle(RoutingContext routingContext) { }); } - if (httpConfiguration.limits.maxBodySize.isPresent()) { - long limit = httpConfiguration.limits.maxBodySize.get().asLongValue(); - Long limitObj = limit; - httpRouteRouter.route().order(-2).handler(new Handler() { - @Override - public void handle(RoutingContext event) { - String lengthString = event.request().headers().get(HttpHeaderNames.CONTENT_LENGTH); - - if (lengthString != null) { - long length = Long.parseLong(lengthString); - if (length > limit) { - event.response().headers().add(HttpHeaderNames.CONNECTION, "close"); - event.response().setStatusCode(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE.code()); - event.response().endHandler(new Handler() { - @Override - public void handle(Void e) { - event.request().connection().close(); - } - }); - event.response().end(); - return; - } - } else { - event.put(MAX_REQUEST_SIZE_KEY, limitObj); - } - event.next(); - } - }); - } + HttpServerCommonHandlers.enforceMaxBodySize(httpConfiguration.limits, httpRouteRouter); // Filter Configuration per path var filtersInConfig = httpConfiguration.filter; - if (!filtersInConfig.isEmpty()) { - for (var entry : filtersInConfig.entrySet()) { - var filterConfig = entry.getValue(); - var matches = filterConfig.matches; - var order = filterConfig.order.orElse(Integer.MIN_VALUE); - var methods = filterConfig.methods; - var headers = filterConfig.header; - if (methods.isEmpty()) { - httpRouteRouter.routeWithRegex(matches) - .order(order) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().setAll(headers); - event.next(); - } - }); - } else { - for (var method : methods.get()) { - httpRouteRouter.routeWithRegex(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), matches) - .order(order) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().setAll(headers); - event.next(); - } - }); - } - } - } - } + HttpServerCommonHandlers.applyFilters(filtersInConfig, httpRouteRouter); // Headers sent on any request, regardless of the response - Map headers = httpConfiguration.header; - if (!headers.isEmpty()) { - // Creates a handler for each header entry - for (Map.Entry entry : headers.entrySet()) { - var name = entry.getKey(); - var config = entry.getValue(); - if (config.methods.isEmpty()) { - httpRouteRouter.route(config.path) - .order(Integer.MIN_VALUE) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().set(name, config.value); - event.next(); - } - }); - } else { - for (String method : config.methods.get()) { - httpRouteRouter.route(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), config.path) - .order(Integer.MIN_VALUE) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().add(name, config.value); - event.next(); - } - }); - } - } - } - } + HttpServerCommonHandlers.applyHeaders(httpConfiguration.header, httpRouteRouter); Handler root; if (rootPath.equals("/")) { @@ -523,6 +435,7 @@ public void handle(RoutingContext event) { Router mainRouter = mainRouterRuntimeValue.isPresent() ? mainRouterRuntimeValue.get().getValue() : Router.router(vertx.get()); mainRouter.mountSubRouter(rootPath, httpRouteRouter); + if (hotReplacementHandler != null) { ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); mainRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @@ -536,25 +449,9 @@ public void handle(RoutingContext event) { root = mainRouter; } - if (httpConfiguration.proxy.proxyAddressForwarding) { - warnIfProxyAddressForwardingAllowedWithMultipleHeaders(httpConfiguration); - final ForwardingProxyOptions forwardingProxyOptions = ForwardingProxyOptions.from(httpConfiguration); - final TrustedProxyCheckBuilder proxyCheckBuilder = forwardingProxyOptions.trustedProxyCheckBuilder; - Handler delegate = root; - if (proxyCheckBuilder == null) { - // no proxy check => we do not restrict who can send `X-Forwarded` or `X-Forwarded-*` headers - final TrustedProxyCheck allowAllProxyCheck = allowAll(); - root = new Handler() { - @Override - public void handle(HttpServerRequest event) { - delegate.handle(new ForwardedServerRequestWrapper(event, forwardingProxyOptions, allowAllProxyCheck)); - } - }; - } else { - // restrict who can send `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers - root = new ForwardedProxyHandler(proxyCheckBuilder, vertx, delegate, forwardingProxyOptions); - } - } + warnIfProxyAddressForwardingAllowedWithMultipleHeaders(httpConfiguration.proxy); + root = HttpServerCommonHandlers.applyProxy(httpConfiguration.proxy, root, vertx); + boolean quarkusWrapperNeeded = false; if (shutdownConfig.isShutdownTimeoutSet()) { @@ -608,27 +505,7 @@ public void handle(HttpServerRequest event) { } Handler delegate = root; - root = new Handler() { - @Override - public void handle(HttpServerRequest event) { - if (!VertxContext.isOnDuplicatedContext()) { - // Vert.x should call us on a duplicated context. - // But in the case of pipelined requests, it does not. - // See https://github.com/quarkusio/quarkus/issues/24626. - Context context = VertxContext.createNewDuplicatedContext(); - context.runOnContext(new Handler() { - @Override - public void handle(Void x) { - setCurrentContextSafe(true); - delegate.handle(new ResumingRequestWrapper(event)); - } - }); - } else { - setCurrentContextSafe(true); - delegate.handle(new ResumingRequestWrapper(event)); - } - } - }; + root = HttpServerCommonHandlers.enforceDuplicatedContext(delegate); if (httpConfiguration.recordRequestStartTime) { httpRouteRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @Override @@ -643,13 +520,44 @@ public void handle(RoutingContext event) { root = remoteSyncHandler = new RemoteSyncHandler(liveReloadConfig.password.get(), root, hotReplacementContext); } rootHandler = root; + + if (managementRouter != null && managementRouter.getValue() != null) { + // Add body handler and cors handler + var mr = managementRouter.getValue(); + mr.route().order(Integer.MIN_VALUE).handler(createBodyHandlerForManagementInterface()); + // We can use "*" here as the management interface is not expected to be used publicly. + mr.route().order(Integer.MIN_VALUE).handler(CorsHandler.create().addOrigin("*")); + + HttpServerCommonHandlers.applyFilters(managementConfiguration.getValue().filter, mr); + HttpServerCommonHandlers.applyHeaders(managementConfiguration.getValue().header, mr); + HttpServerCommonHandlers.enforceMaxBodySize(managementConfiguration.getValue().limits, mr); + applyCompression(managementBuildTimeConfig.enableCompression, mr); + + Handler handler = HttpServerCommonHandlers.enforceDuplicatedContext(mr); + handler = HttpServerCommonHandlers.applyProxy(managementConfiguration.getValue().proxy, handler, vertx); + + VertxHttpRecorder.managementRouter = handler; + } + } + + private void applyCompression(boolean enableCompression, Router httpRouteRouter) { + if (enableCompression) { + httpRouteRouter.route().order(0).handler(new Handler() { + @Override + public void handle(RoutingContext ctx) { + // Add "Content-Encoding: identity" header that disables the compression + // This header can be removed to enable the compression + ctx.response().putHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY); + ctx.next(); + } + }); + } } - private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(HttpConfiguration httpConfiguration) { - ProxyConfig proxyConfig = httpConfiguration.proxy; + private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(ProxyConfig proxyConfig) { boolean proxyAddressForwardingActivated = proxyConfig.proxyAddressForwarding; boolean forwardedActivated = proxyConfig.allowForwarded; - boolean xForwardedActivated = httpConfiguration.proxy.allowXForwarded.orElse(!forwardedActivated); + boolean xForwardedActivated = proxyConfig.allowXForwarded.orElse(!forwardedActivated); if (proxyAddressForwardingActivated && forwardedActivated && xForwardedActivated) { LOGGER.warn( @@ -660,16 +568,95 @@ private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(HttpConfigur } } - private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, - HttpConfiguration httpConfiguration, LaunchMode launchMode, - Supplier eventLoops, List websocketSubProtocols, boolean auxiliaryApplication) throws IOException { + private static CompletableFuture initializeManagementInterfaceWithDomainSocket(Vertx vertx, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, + ManagementInterfaceConfiguration managementConfig, + List websocketSubProtocols) { + CompletableFuture managementInterfaceDomainSocketFuture = new CompletableFuture<>(); + if (!managementBuildTimeConfig.enabled || managementRouter == null || managementConfig == null) { + managementInterfaceDomainSocketFuture.complete(null); + return managementInterfaceDomainSocketFuture; + } + + HttpServerOptions domainSocketOptionsForManagement = createDomainSocketOptionsForManagementInterface( + managementBuildTimeConfig, managementConfig, + websocketSubProtocols); + if (domainSocketOptionsForManagement != null) { + vertx.createHttpServer(domainSocketOptionsForManagement) + .requestHandler(managementRouter) + .listen(ar -> { + if (ar.failed()) { + managementInterfaceDomainSocketFuture.completeExceptionally( + new IllegalStateException( + "Unable to start the management interface on the " + + domainSocketOptionsForManagement.getHost() + " domain socket", + ar.cause())); + } else { + managementInterfaceDomainSocketFuture.complete(ar.result()); + } + }); + } else { + managementInterfaceDomainSocketFuture.complete(null); + } + return managementInterfaceDomainSocketFuture; + } + + private static CompletableFuture initializeManagementInterface(Vertx vertx, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, + ManagementInterfaceConfiguration managementConfig, + LaunchMode launchMode, + List websocketSubProtocols) throws IOException { + httpManagementServerOptions = null; + CompletableFuture managementInterfaceFuture = new CompletableFuture<>(); + if (!managementBuildTimeConfig.enabled || managementRouter == null || managementConfig == null) { + managementInterfaceFuture.complete(null); + return managementInterfaceFuture; + } + + HttpServerOptions httpServerOptionsForManagement = createHttpServerOptionsForManagementInterface( + managementBuildTimeConfig, managementConfig, launchMode, + websocketSubProtocols); + httpManagementServerOptions = HttpServerOptionsUtils.createSslOptionsForManagementInterface( + managementBuildTimeConfig, managementConfig, launchMode, + websocketSubProtocols); + if (httpManagementServerOptions != null && httpManagementServerOptions.getKeyCertOptions() == null) { + httpManagementServerOptions = httpServerOptionsForManagement; + } + + if (httpManagementServerOptions != null) { + vertx.createHttpServer(httpManagementServerOptions) + .requestHandler(managementRouter) + .listen(ar -> { + if (ar.failed()) { + managementInterfaceFuture.completeExceptionally( + new IllegalStateException("Unable to start the management interface", ar.cause())); + } else { + actualManagementPort = ar.result().actualPort(); + managementInterfaceFuture.complete(ar.result()); + } + }); + } else { + managementInterfaceFuture.complete(null); + } + return managementInterfaceFuture; + } + + private static CompletableFuture initializeMainHttpServer(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, + HttpConfiguration httpConfiguration, + LaunchMode launchMode, + Supplier eventLoops, List websocketSubProtocols) throws IOException { + + if (!httpConfiguration.hostEnabled) { + return CompletableFuture.completedFuture(null); + } // Http server configuration - HttpServerOptions httpServerOptions = createHttpServerOptions(httpBuildTimeConfig, httpConfiguration, launchMode, + httpMainServerOptions = createHttpServerOptions(httpBuildTimeConfig, httpConfiguration, launchMode, websocketSubProtocols); - HttpServerOptions domainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration, + httpMainDomainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration, websocketSubProtocols); - HttpServerOptions tmpSslConfig = createSslOptions(httpBuildTimeConfig, httpConfiguration, launchMode, + HttpServerOptions tmpSslConfig = HttpServerOptionsUtils.createSslOptions(httpBuildTimeConfig, httpConfiguration, + launchMode, websocketSubProtocols); // Customize @@ -678,14 +665,14 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime .listAll(HttpServerOptionsCustomizer.class); for (InstanceHandle instance : instances) { HttpServerOptionsCustomizer customizer = instance.get(); - if (httpServerOptions != null) { - customizer.customizeHttpServer(httpServerOptions); + if (httpMainServerOptions != null) { + customizer.customizeHttpServer(httpMainServerOptions); } if (tmpSslConfig != null) { customizer.customizeHttpsServer(tmpSslConfig); } - if (domainSocketOptions != null) { - customizer.customizeDomainSocketServer(domainSocketOptions); + if (httpMainDomainSocketOptions != null) { + customizer.customizeDomainSocketServer(httpMainDomainSocketOptions); } } } @@ -694,9 +681,10 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime if (tmpSslConfig != null && tmpSslConfig.getKeyCertOptions() == null) { tmpSslConfig = null; } - final HttpServerOptions sslConfig = tmpSslConfig; + httpMainSslServerOptions = tmpSslConfig; - if (httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED && sslConfig == null) { + if (httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED + && httpMainSslServerOptions == null) { throw new IllegalStateException("Cannot set quarkus.http.redirect-insecure-requests without enabling SSL."); } @@ -710,11 +698,13 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime ioThreads = eventLoopCount; } CompletableFuture futureResult = new CompletableFuture<>(); + AtomicInteger connectionCount = new AtomicInteger(); vertx.deployVerticle(new Supplier() { @Override public Verticle get() { - return new WebDeploymentVerticle(httpServerOptions, sslConfig, domainSocketOptions, launchMode, + return new WebDeploymentVerticle(httpMainServerOptions, httpMainSslServerOptions, httpMainDomainSocketOptions, + launchMode, httpConfiguration.insecureRequests, httpConfiguration, connectionCount); } }, new DeploymentOptions().setInstances(ioThreads), new Handler>() { @@ -725,13 +715,15 @@ public void handle(AsyncResult event) { if (effectiveCause instanceof BindException) { List portsUsed = Collections.emptyList(); - if ((sslConfig == null) && (httpServerOptions != null)) { - portsUsed = List.of(httpServerOptions.getPort()); - } else if ((httpConfiguration.insecureRequests == InsecureRequests.DISABLED) && (sslConfig != null)) { - portsUsed = List.of(sslConfig.getPort()); - } else if ((sslConfig != null) && (httpConfiguration.insecureRequests == InsecureRequests.ENABLED) - && (httpServerOptions != null)) { - portsUsed = List.of(httpServerOptions.getPort(), sslConfig.getPort()); + if ((httpMainSslServerOptions == null) && (httpMainServerOptions != null)) { + portsUsed = List.of(httpMainServerOptions.getPort()); + } else if ((httpConfiguration.insecureRequests == InsecureRequests.DISABLED) + && (httpMainSslServerOptions != null)) { + portsUsed = List.of(httpMainSslServerOptions.getPort()); + } else if ((httpMainSslServerOptions != null) + && (httpConfiguration.insecureRequests == InsecureRequests.ENABLED) + && (httpMainServerOptions != null)) { + portsUsed = List.of(httpMainServerOptions.getPort(), httpMainSslServerOptions.getPort()); } effectiveCause = new QuarkusBindException((BindException) effectiveCause, portsUsed); @@ -742,51 +734,107 @@ public void handle(AsyncResult event) { } } }); + + return futureResult; + } + + private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, + HttpConfiguration httpConfiguration, ManagementInterfaceConfiguration managementConfig, + LaunchMode launchMode, + Supplier eventLoops, List websocketSubProtocols, boolean auxiliaryApplication) throws IOException { + + var mainServerFuture = initializeMainHttpServer(vertx, httpBuildTimeConfig, httpConfiguration, launchMode, eventLoops, + websocketSubProtocols); + var managementInterfaceFuture = initializeManagementInterface(vertx, managementBuildTimeConfig, managementRouter, + managementConfig, launchMode, websocketSubProtocols); + var managementInterfaceDomainSocketFuture = initializeManagementInterfaceWithDomainSocket(vertx, + managementBuildTimeConfig, managementRouter, managementConfig, websocketSubProtocols); + try { - String deploymentId = futureResult.get(); - VertxCoreRecorder.setWebDeploymentId(deploymentId); + String deploymentIdIfAny = mainServerFuture.get(); + + HttpServer tmpManagementServer = null; + HttpServer tmpManagementServerUsingDomainSocket = null; + if (managementRouter != null) { + tmpManagementServer = managementInterfaceFuture.get(); + tmpManagementServerUsingDomainSocket = managementInterfaceDomainSocketFuture.get(); + } + HttpServer managementServer = tmpManagementServer; + HttpServer managementServerDomainSocket = tmpManagementServerUsingDomainSocket; + if (deploymentIdIfAny != null) { + VertxCoreRecorder.setWebDeploymentId(deploymentIdIfAny); + } closeTask = new Runnable() { @Override public synchronized void run() { //guard against this being run twice if (closeTask == this) { - if (vertx.deploymentIDs().contains(deploymentId)) { - CountDownLatch latch = new CountDownLatch(1); + int count = 0; + if (deploymentIdIfAny != null && vertx.deploymentIDs().contains(deploymentIdIfAny)) { + count++; + } + if (managementServer != null) { + count++; + } + if (managementServerDomainSocket != null) { + count++; + } + + CountDownLatch latch = new CountDownLatch(count); + var handler = new Handler>() { + @Override + public void handle(AsyncResult event) { + latch.countDown(); + } + }; + + // shutdown main HTTP server + if (deploymentIdIfAny != null) { try { - vertx.undeploy(deploymentId, new Handler>() { - @Override - public void handle(AsyncResult event) { - latch.countDown(); - } - }); + vertx.undeploy(deploymentIdIfAny, handler); } catch (Exception e) { LOGGER.warn("Failed to undeploy deployment ", e); } - try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + } + + // shutdown the management interface + try { + if (managementServer != null) { + managementServer.close(handler); + } + if (managementServerDomainSocket != null) { + managementServerDomainSocket.close(handler); } + } catch (Exception e) { + LOGGER.warn("Unable to shutdown the management interface quietly", e); } - closeTask = null; - if (remoteSyncHandler != null) { - remoteSyncHandler.close(); - remoteSyncHandler = null; + + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); } } + closeTask = null; + if (remoteSyncHandler != null) { + remoteSyncHandler.close(); + remoteSyncHandler = null; + } } }; } catch (InterruptedException | ExecutionException e) { throw new RuntimeException("Unable to start HTTP server", e); } - setHttpServerTiming(httpConfiguration.insecureRequests, httpServerOptions, sslConfig, domainSocketOptions, - auxiliaryApplication); + setHttpServerTiming(httpConfiguration.insecureRequests, httpMainServerOptions, httpMainSslServerOptions, + httpMainDomainSocketOptions, + auxiliaryApplication, httpManagementServerOptions); } private static void setHttpServerTiming(InsecureRequests insecureRequests, HttpServerOptions httpServerOptions, HttpServerOptions sslConfig, - HttpServerOptions domainSocketOptions, boolean auxiliaryApplication) { + HttpServerOptions domainSocketOptions, boolean auxiliaryApplication, HttpServerOptions managementConfig) { StringBuilder serverListeningMessage = new StringBuilder("Listening on: "); int socketCount = 0; @@ -810,239 +858,43 @@ private static void setHttpServerTiming(InsecureRequests insecureRequests, HttpS } serverListeningMessage.append(String.format("unix:%s", domainSocketOptions.getHost())); } + if (managementConfig != null) { + serverListeningMessage.append( + String.format(". Management interface listening on http:%s//%s:%s.", managementConfig.isSsl() ? "s" : "", + managementConfig.getHost(), managementConfig.getPort())); + } + Timing.setHttpServer(serverListeningMessage.toString(), auxiliaryApplication); } - /** - * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled - */ - private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, - LaunchMode launchMode, List websocketSubProtocols) - throws IOException { + private static HttpServerOptions createHttpServerOptions( + HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, + LaunchMode launchMode, List websocketSubProtocols) { if (!httpConfiguration.hostEnabled) { return null; } + // TODO other config properties + HttpServerOptions options = new HttpServerOptions(); + int port = httpConfiguration.determinePort(launchMode); + options.setPort(port == 0 ? -1 : port); - ServerSslConfig sslConfig = httpConfiguration.ssl; - - final Optional certFile = sslConfig.certificate.file; - final Optional keyFile = sslConfig.certificate.keyFile; - final List keys = new ArrayList<>(); - final List certificates = new ArrayList<>(); - if (sslConfig.certificate.keyFiles.isPresent()) { - keys.addAll(sslConfig.certificate.keyFiles.get()); - } - if (sslConfig.certificate.files.isPresent()) { - certificates.addAll(sslConfig.certificate.files.get()); - } - if (keyFile.isPresent()) { - keys.add(keyFile.get()); - } - if (certFile.isPresent()) { - certificates.add(certFile.get()); - } - - // credentials provider - Map credentials = Map.of(); - if (sslConfig.certificate.credentialsProvider.isPresent()) { - String beanName = sslConfig.certificate.credentialsProviderName.orElse(null); - CredentialsProvider credentialsProvider = CredentialsProviderFinder.find(beanName); - String name = sslConfig.certificate.credentialsProvider.get(); - credentials = credentialsProvider.getCredentials(name); - } - final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; - final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials, - sslConfig.certificate.keyStorePasswordKey); - final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials, - sslConfig.certificate.keyStoreKeyPasswordKey); - final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; - final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials, - sslConfig.certificate.trustStorePasswordKey); - final HttpServerOptions serverOptions = new HttpServerOptions(); - - //ssl - if (JdkSSLEngineOptions.isAlpnAvailable()) { - serverOptions.setUseAlpn(httpConfiguration.http2); - if (httpConfiguration.http2) { - serverOptions.setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); - } - } - setIdleTimeout(httpConfiguration, serverOptions); - - if (!certificates.isEmpty() && !keys.isEmpty()) { - createPemKeyCertOptions(certificates, keys, serverOptions); - } else if (keyStoreFile.isPresent()) { - KeyStoreOptions options = createKeyStoreOptions( - keyStoreFile.get(), - keyStorePassword.orElse("password"), - sslConfig.certificate.keyStoreFileType, - sslConfig.certificate.keyStoreProvider, - sslConfig.certificate.keyStoreKeyAlias, - keyStoreKeyPassword); - serverOptions.setKeyCertOptions(options); - } - - if (trustStoreFile.isPresent()) { - if (!trustStorePassword.isPresent()) { - throw new IllegalArgumentException("No trust store password provided"); - } - KeyStoreOptions options = createKeyStoreOptions( - trustStoreFile.get(), - trustStorePassword.get(), - sslConfig.certificate.trustStoreFileType, - sslConfig.certificate.trustStoreProvider, - sslConfig.certificate.trustStoreCertAlias, - Optional.empty()); - serverOptions.setTrustOptions(options); - } - - for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) { - serverOptions.addEnabledCipherSuite(cipher); - } - - for (String protocol : sslConfig.protocols) { - if (!protocol.isEmpty()) { - serverOptions.addEnabledSecureTransportProtocol(protocol); - } - } - serverOptions.setSsl(true); - serverOptions.setSni(sslConfig.sni); - int sslPort = httpConfiguration.determineSslPort(launchMode); - // -2 instead of -1 (see http) to have vert.x assign two different random ports if both http and https shall be random - serverOptions.setPort(sslPort == 0 ? -2 : sslPort); - serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); - - applyCommonOptions(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); - - return serverOptions; - } - - private static Optional getCredential(Optional password, Map credentials, - Optional passwordKey) { - if (password.isPresent()) { - return password; - } - - if (passwordKey.isPresent()) { - return Optional.ofNullable(credentials.get(passwordKey.get())); - } else { - return Optional.empty(); - } - } - - private static void applyCommonOptions(HttpServerOptions httpServerOptions, - HttpBuildTimeConfig buildTimeConfig, - HttpConfiguration httpConfiguration, - List websocketSubProtocols) { - httpServerOptions.setHost(httpConfiguration.host); - setIdleTimeout(httpConfiguration, httpServerOptions); - httpServerOptions.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); - httpServerOptions.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); - httpServerOptions.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); - httpServerOptions.setWebSocketSubProtocols(websocketSubProtocols); - httpServerOptions.setReusePort(httpConfiguration.soReusePort); - httpServerOptions.setTcpQuickAck(httpConfiguration.tcpQuickAck); - httpServerOptions.setTcpCork(httpConfiguration.tcpCork); - httpServerOptions.setAcceptBacklog(httpConfiguration.acceptBacklog); - httpServerOptions.setTcpFastOpen(httpConfiguration.tcpFastOpen); - httpServerOptions.setCompressionSupported(buildTimeConfig.enableCompression); - if (buildTimeConfig.compressionLevel.isPresent()) { - httpServerOptions.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); - } - httpServerOptions.setDecompressionSupported(buildTimeConfig.enableDecompression); - httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); - httpServerOptions.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); - } - - private static KeyStoreOptions createKeyStoreOptions(Path path, String password, Optional fileType, - Optional provider, Optional alias, Optional aliasPassword) throws IOException { - final String type; - if (fileType.isPresent()) { - type = fileType.get().toLowerCase(); - } else { - type = findKeystoreFileType(path); - } + HttpServerOptionsUtils.applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); - byte[] data = getFileContent(path); - KeyStoreOptions options = new KeyStoreOptions() - .setPassword(password) - .setValue(Buffer.buffer(data)) - .setType(type.toUpperCase()) - .setProvider(provider.orElse(null)) - .setAlias(alias.orElse(null)) - .setAliasPassword(aliasPassword.orElse(null)); return options; } - private static byte[] getFileContent(Path path) throws IOException { - byte[] data; - final InputStream resource = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(ClassPathUtils.toResourceName(path)); - if (resource != null) { - try (InputStream is = resource) { - data = doRead(is); - } - } else { - try (InputStream is = Files.newInputStream(path)) { - data = doRead(is); - } - } - return data; - } - - private static void createPemKeyCertOptions(List certFile, List keyFile, - HttpServerOptions serverOptions) throws IOException { - - if (certFile.size() != keyFile.size()) { - throw new ConfigurationException("Invalid certificate configuration - `files` and `keyFiles` must have the " - + "same number of elements"); - } - - List certificates = new ArrayList<>(); - List keys = new ArrayList<>(); - - for (Path p : certFile) { - final byte[] cert = getFileContent(p); - certificates.add(Buffer.buffer(cert)); - } - - for (Path p : keyFile) { - final byte[] key = getFileContent(p); - keys.add(Buffer.buffer(key)); - } - - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setCertValues(certificates) - .setKeyValues(keys); - serverOptions.setPemKeyCertOptions(pemKeyCertOptions); - } - - private static String findKeystoreFileType(Path storePath) { - final String pathName = storePath.toString(); - if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) { - return "pkcs12"; - } else { - // assume jks - return "jks"; - } - } - - private static byte[] doRead(InputStream is) throws IOException { - return is.readAllBytes(); - } - - private static HttpServerOptions createHttpServerOptions( - HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, + private static HttpServerOptions createHttpServerOptionsForManagementInterface( + ManagementInterfaceBuildTimeConfig buildTimeConfig, ManagementInterfaceConfiguration httpConfiguration, LaunchMode launchMode, List websocketSubProtocols) { if (!httpConfiguration.hostEnabled) { return null; } - // TODO other config properties HttpServerOptions options = new HttpServerOptions(); int port = httpConfiguration.determinePort(launchMode); options.setPort(port == 0 ? -1 : port); - applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); + HttpServerOptionsUtils.applyCommonOptionsForManagementInterface(options, buildTimeConfig, httpConfiguration, + websocketSubProtocols); return options; } @@ -1055,7 +907,7 @@ private static HttpServerOptions createDomainSocketOptions( } HttpServerOptions options = new HttpServerOptions(); - applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); + HttpServerOptionsUtils.applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); // Override the host (0.0.0.0 by default) with the configured domain socket. options.setHost(httpConfiguration.domainSocket); @@ -1071,25 +923,42 @@ private static HttpServerOptions createDomainSocketOptions( return options; } - private static void setIdleTimeout(HttpConfiguration httpConfiguration, HttpServerOptions options) { - int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); - options.setIdleTimeout(idleTimeout); - options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + private static HttpServerOptions createDomainSocketOptionsForManagementInterface( + ManagementInterfaceBuildTimeConfig buildTimeConfig, ManagementInterfaceConfiguration httpConfiguration, + List websocketSubProtocols) { + if (!httpConfiguration.domainSocketEnabled) { + return null; + } + HttpServerOptions options = new HttpServerOptions(); + + HttpServerOptionsUtils.applyCommonOptionsForManagementInterface(options, buildTimeConfig, httpConfiguration, + websocketSubProtocols); + // Override the host (0.0.0.0 by default) with the configured domain socket. + options.setHost(httpConfiguration.domainSocket); + + // Check if we can write into the domain socket directory + // We can do this check using a blocking API as the execution is done from the main thread (not an I/O thread) + File file = new File(httpConfiguration.domainSocket); + if (!file.getParentFile().canWrite()) { + LOGGER.warnf( + "Unable to write in the domain socket directory (`%s`). Binding to the socket is likely going to fail.", + httpConfiguration.domainSocket); + } + + return options; } public void addRoute(RuntimeValue router, Function route, Handler handler, - HandlerType blocking) { + HandlerType type) { Route vr = route.apply(router.getValue()); - - if (blocking == HandlerType.BLOCKING) { + if (type == HandlerType.BLOCKING) { vr.blockingHandler(handler, false); - } else if (blocking == HandlerType.FAILURE) { + } else if (type == HandlerType.FAILURE) { vr.failureHandler(handler); } else { vr.handler(handler); } - } public void setNonApplicationRedirectHandler(String nonApplicationPath, String rootPath) { @@ -1411,6 +1280,7 @@ public void afterRestore(org.crac.Context context) throws Ex p.future().onComplete(event -> latch.countDown()); latch.await(); } + } protected static ServerBootstrap virtualBootstrap; @@ -1480,13 +1350,11 @@ public static Object getCurrentApplicationState() { return rootHandler; } - public Handler createBodyHandler() { + private static Handler configureAndGetBody(Optional maxBodySize, BodyConfig bodyConfig) { BodyHandler bodyHandler = BodyHandler.create(); - Optional maxBodySize = httpConfiguration.getValue().limits.maxBodySize; if (maxBodySize.isPresent()) { bodyHandler.setBodyLimit(maxBodySize.get().asLongValue()); } - final BodyConfig bodyConfig = httpConfiguration.getValue().body; bodyHandler.setHandleFileUploads(bodyConfig.handleFileUploads); bodyHandler.setUploadsDirectory(bodyConfig.uploadsDirectory); bodyHandler.setDeleteUploadedFilesOnEnd(bodyConfig.deleteUploadedFilesOnEnd); @@ -1530,6 +1398,16 @@ public void run() { }; } + public Handler createBodyHandler() { + Optional maxBodySize = httpConfiguration.getValue().limits.maxBodySize; + return configureAndGetBody(maxBodySize, httpConfiguration.getValue().body); + } + + public Handler createBodyHandlerForManagementInterface() { + Optional maxBodySize = managementConfiguration.getValue().limits.maxBodySize; + return configureAndGetBody(maxBodySize, managementConfiguration.getValue().body); + } + private static final List CAN_HAVE_BODY = Arrays.asList(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java new file mode 100644 index 00000000000000..6ee7b2a25622fa --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java @@ -0,0 +1,68 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.vertx.http.runtime.AuthConfig; +import io.vertx.core.http.ClientAuth; + +/** + * Management interface configuration. + */ +@ConfigRoot(name = "management", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class ManagementInterfaceBuildTimeConfig { + + public AuthConfig auth; + + /** + * Enables / Disables the usage of a separate interface/port to expose the management endpoints. + * If sets to {@code true}, the management endpoints will be exposed to a different HTTP server. + * This avoids exposing the management endpoints on a publicly available server. + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * Configures the engine to require/request client authentication. + * NONE, REQUEST, REQUIRED + */ + @ConfigItem(name = "ssl.client-auth", defaultValue = "NONE") + public ClientAuth tlsClientAuth; + + /** + * A common root path for management endpoints. Various extension-provided management endpoints such as metrics + * and health are deployed under this path by default. + */ + @ConfigItem(defaultValue = "/q") + public String rootPath; + + /** + * If responses should be compressed. + *

    + * Note that this will attempt to compress all responses, to avoid compressing + * already compressed content (such as images) you need to set the following header: + *

    + * Content-Encoding: identity + *

    + * Which will tell vert.x not to compress the response. + */ + @ConfigItem + public boolean enableCompression; + + /** + * When enabled, vert.x will decompress the request's body if it's compressed. + *

    + * Note that the compression format (e.g., gzip) must be specified in the Content-Encoding header + * in the request. + */ + @ConfigItem + public boolean enableDecompression; + + /** + * The compression level used when compression support is enabled. + */ + @ConfigItem + public OptionalInt compressionLevel; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java new file mode 100644 index 00000000000000..9d77f458d1c901 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java @@ -0,0 +1,120 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.vertx.http.runtime.BodyConfig; +import io.quarkus.vertx.http.runtime.FilterConfig; +import io.quarkus.vertx.http.runtime.HeaderConfig; +import io.quarkus.vertx.http.runtime.ProxyConfig; +import io.quarkus.vertx.http.runtime.ServerLimitsConfig; +import io.quarkus.vertx.http.runtime.ServerSslConfig; + +/** + * Configures the management interface. + * Note that the management interface must be enabled using the + * {@link ManagementInterfaceBuildTimeConfig#enabled} build-time property. + */ +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "management") +public class ManagementInterfaceConfiguration { + + /** + * The HTTP port + */ + @ConfigItem(defaultValue = "9000") + public int port; + + /** + * The HTTP port + */ + @ConfigItem(defaultValue = "9001") + public int testPort; + + /** + * The HTTP host + * + * Defaults to 0.0.0.0 + * + * Defaulting to 0.0.0.0 makes it easier to deploy Quarkus to container, however it + * is not suitable for dev/test mode as other people on the network can connect to your + * development machine. + */ + @ConfigItem + public Optional host; + + /** + * Enable listening to host:port + */ + @ConfigItem(defaultValue = "true") + public boolean hostEnabled; + + /** + * The SSL config + */ + public ServerSslConfig ssl; + + /** + * When set to {@code true}, the HTTP server automatically sends `100 CONTINUE` + * response when the request expects it (with the `Expect: 100-Continue` header). + */ + @ConfigItem(defaultValue = "false", name = "handle-100-continue-automatically") + public boolean handle100ContinueAutomatically; + + /** + * Server limits configuration + */ + public ServerLimitsConfig limits; + + /** + * Http connection idle timeout + */ + @ConfigItem(defaultValue = "30M", name = "idle-timeout") + public Duration idleTimeout; + + /** + * Request body related settings + */ + public BodyConfig body; + + /** + * The accept backlog, this is how many connections can be waiting to be accepted before connections start being rejected + */ + @ConfigItem(defaultValue = "-1") + public int acceptBacklog; + + /** + * Path to a unix domain socket + */ + @ConfigItem(defaultValue = "/var/run/io.quarkus.management.socket") + public String domainSocket; + + /** + * Enable listening to host:port + */ + @ConfigItem + public boolean domainSocketEnabled; + + /** + * Additional HTTP Headers always sent in the response + */ + @ConfigItem + public Map header; + + /** + * Additional HTTP configuration per path + */ + @ConfigItem + public Map filter; + + public ProxyConfig proxy; + + public int determinePort(LaunchMode launchMode) { + return launchMode == LaunchMode.TEST ? testPort : port; + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java new file mode 100644 index 00000000000000..562611de71eb49 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java @@ -0,0 +1,177 @@ +package io.quarkus.vertx.http.runtime.options; + +import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setCurrentContextSafe; +import static io.quarkus.vertx.http.runtime.TrustedProxyCheck.allowAll; + +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.vertx.http.runtime.FilterConfig; +import io.quarkus.vertx.http.runtime.ForwardedProxyHandler; +import io.quarkus.vertx.http.runtime.ForwardedServerRequestWrapper; +import io.quarkus.vertx.http.runtime.ForwardingProxyOptions; +import io.quarkus.vertx.http.runtime.HeaderConfig; +import io.quarkus.vertx.http.runtime.ProxyConfig; +import io.quarkus.vertx.http.runtime.ResumingRequestWrapper; +import io.quarkus.vertx.http.runtime.ServerLimitsConfig; +import io.quarkus.vertx.http.runtime.TrustedProxyCheck; +import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.smallrye.common.vertx.VertxContext; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class HttpServerCommonHandlers { + public static void enforceMaxBodySize(ServerLimitsConfig limits, Router httpRouteRouter) { + if (limits.maxBodySize.isPresent()) { + long limit = limits.maxBodySize.get().asLongValue(); + Long limitObj = limit; + httpRouteRouter.route().order(-2).handler(new Handler() { + @Override + public void handle(RoutingContext event) { + String lengthString = event.request().headers().get(HttpHeaderNames.CONTENT_LENGTH); + + if (lengthString != null) { + long length = Long.parseLong(lengthString); + if (length > limit) { + event.response().headers().add(HttpHeaderNames.CONNECTION, "close"); + event.response().setStatusCode(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE.code()); + event.response().endHandler(new Handler() { + @Override + public void handle(Void e) { + event.request().connection().close(); + } + }); + event.response().end(); + return; + } + } else { + event.put(VertxHttpRecorder.MAX_REQUEST_SIZE_KEY, limitObj); + } + event.next(); + } + }); + } + } + + public static Handler enforceDuplicatedContext(Handler delegate) { + return new Handler() { + @Override + public void handle(HttpServerRequest event) { + if (!VertxContext.isOnDuplicatedContext()) { + // Vert.x should call us on a duplicated context. + // But in the case of pipelined requests, it does not. + // See https://github.com/quarkusio/quarkus/issues/24626. + Context context = VertxContext.createNewDuplicatedContext(); + context.runOnContext(new Handler() { + @Override + public void handle(Void x) { + setCurrentContextSafe(true); + delegate.handle(new ResumingRequestWrapper(event)); + } + }); + } else { + setCurrentContextSafe(true); + delegate.handle(new ResumingRequestWrapper(event)); + } + } + }; + } + + public static Handler applyProxy(ProxyConfig proxyConfig, Handler root, + Supplier vertx) { + if (proxyConfig.proxyAddressForwarding) { + final ForwardingProxyOptions forwardingProxyOptions = ForwardingProxyOptions.from(proxyConfig); + final TrustedProxyCheck.TrustedProxyCheckBuilder proxyCheckBuilder = forwardingProxyOptions.trustedProxyCheckBuilder; + if (proxyCheckBuilder == null) { + // no proxy check => we do not restrict who can send `X-Forwarded` or `X-Forwarded-*` headers + final TrustedProxyCheck allowAllProxyCheck = allowAll(); + return new Handler() { + @Override + public void handle(HttpServerRequest event) { + root.handle(new ForwardedServerRequestWrapper(event, forwardingProxyOptions, allowAllProxyCheck)); + } + }; + } else { + // restrict who can send `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers + return new ForwardedProxyHandler(proxyCheckBuilder, vertx, root, forwardingProxyOptions); + } + } + return root; + } + + public static void applyFilters(Map filtersInConfig, Router httpRouteRouter) { + if (!filtersInConfig.isEmpty()) { + for (var entry : filtersInConfig.entrySet()) { + var filterConfig = entry.getValue(); + var matches = filterConfig.matches; + var order = filterConfig.order.orElse(Integer.MIN_VALUE); + var methods = filterConfig.methods; + var headers = filterConfig.header; + if (methods.isEmpty()) { + httpRouteRouter.routeWithRegex(matches) + .order(order) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().setAll(headers); + event.next(); + } + }); + } else { + for (var method : methods.get()) { + httpRouteRouter.routeWithRegex(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), matches) + .order(order) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().setAll(headers); + event.next(); + } + }); + } + } + } + } + } + + public static void applyHeaders(Map headers, Router httpRouteRouter) { + if (!headers.isEmpty()) { + // Creates a handler for each header entry + for (Map.Entry entry : headers.entrySet()) { + var name = entry.getKey(); + var config = entry.getValue(); + if (config.methods.isEmpty()) { + httpRouteRouter.route(config.path) + .order(Integer.MIN_VALUE) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().set(name, config.value); + event.next(); + } + }); + } else { + for (String method : config.methods.get()) { + httpRouteRouter.route(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), config.path) + .order(Integer.MIN_VALUE) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().add(name, config.value); + event.next(); + } + }); + } + } + } + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java new file mode 100644 index 00000000000000..b9c522270fb30c --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java @@ -0,0 +1,386 @@ +package io.quarkus.vertx.http.runtime.options; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.quarkus.credentials.CredentialsProvider; +import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.ServerSslConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.JdkSSLEngineOptions; +import io.vertx.core.net.KeyStoreOptions; +import io.vertx.core.net.PemKeyCertOptions; + +public class HttpServerOptionsUtils { + + /** + * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled + */ + public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, + LaunchMode launchMode, List websocketSubProtocols) + throws IOException { + if (!httpConfiguration.hostEnabled) { + return null; + } + + ServerSslConfig sslConfig = httpConfiguration.ssl; + + final Optional certFile = sslConfig.certificate.file; + final Optional keyFile = sslConfig.certificate.keyFile; + final List keys = new ArrayList<>(); + final List certificates = new ArrayList<>(); + if (sslConfig.certificate.keyFiles.isPresent()) { + keys.addAll(sslConfig.certificate.keyFiles.get()); + } + if (sslConfig.certificate.files.isPresent()) { + certificates.addAll(sslConfig.certificate.files.get()); + } + if (keyFile.isPresent()) { + keys.add(keyFile.get()); + } + if (certFile.isPresent()) { + certificates.add(certFile.get()); + } + + // credentials provider + Map credentials = Map.of(); + if (sslConfig.certificate.credentialsProvider.isPresent()) { + String beanName = sslConfig.certificate.credentialsProviderName.orElse(null); + CredentialsProvider credentialsProvider = CredentialsProviderFinder.find(beanName); + String name = sslConfig.certificate.credentialsProvider.get(); + credentials = credentialsProvider.getCredentials(name); + } + final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; + final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials, + sslConfig.certificate.keyStorePasswordKey); + final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials, + sslConfig.certificate.keyStoreKeyPasswordKey); + final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; + final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials, + sslConfig.certificate.trustStorePasswordKey); + final HttpServerOptions serverOptions = new HttpServerOptions(); + + //ssl + if (JdkSSLEngineOptions.isAlpnAvailable()) { + serverOptions.setUseAlpn(httpConfiguration.http2); + if (httpConfiguration.http2) { + serverOptions.setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); + } + } + setIdleTimeout(httpConfiguration, serverOptions); + + if (!certificates.isEmpty() && !keys.isEmpty()) { + createPemKeyCertOptions(certificates, keys, serverOptions); + } else if (keyStoreFile.isPresent()) { + KeyStoreOptions options = createKeyStoreOptions( + keyStoreFile.get(), + keyStorePassword.orElse("password"), + sslConfig.certificate.keyStoreFileType, + sslConfig.certificate.keyStoreProvider, + sslConfig.certificate.keyStoreKeyAlias, + keyStoreKeyPassword); + serverOptions.setKeyCertOptions(options); + } + + if (trustStoreFile.isPresent()) { + if (!trustStorePassword.isPresent()) { + throw new IllegalArgumentException("No trust store password provided"); + } + KeyStoreOptions options = createKeyStoreOptions( + trustStoreFile.get(), + trustStorePassword.get(), + sslConfig.certificate.trustStoreFileType, + sslConfig.certificate.trustStoreProvider, + sslConfig.certificate.trustStoreCertAlias, + Optional.empty()); + serverOptions.setTrustOptions(options); + } + + for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) { + serverOptions.addEnabledCipherSuite(cipher); + } + + for (String protocol : sslConfig.protocols) { + if (!protocol.isEmpty()) { + serverOptions.addEnabledSecureTransportProtocol(protocol); + } + } + serverOptions.setSsl(true); + serverOptions.setSni(sslConfig.sni); + int sslPort = httpConfiguration.determineSslPort(launchMode); + // -2 instead of -1 (see http) to have vert.x assign two different random ports if both http and https shall be random + serverOptions.setPort(sslPort == 0 ? -2 : sslPort); + serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); + + applyCommonOptions(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); + + return serverOptions; + } + + /** + * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled + */ + public static HttpServerOptions createSslOptionsForManagementInterface(ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration httpConfiguration, + LaunchMode launchMode, List websocketSubProtocols) + throws IOException { + if (!httpConfiguration.hostEnabled) { + return null; + } + + ServerSslConfig sslConfig = httpConfiguration.ssl; + + final Optional certFile = sslConfig.certificate.file; + final Optional keyFile = sslConfig.certificate.keyFile; + final List keys = new ArrayList<>(); + final List certificates = new ArrayList<>(); + if (sslConfig.certificate.keyFiles.isPresent()) { + keys.addAll(sslConfig.certificate.keyFiles.get()); + } + if (sslConfig.certificate.files.isPresent()) { + certificates.addAll(sslConfig.certificate.files.get()); + } + if (keyFile.isPresent()) { + keys.add(keyFile.get()); + } + if (certFile.isPresent()) { + certificates.add(certFile.get()); + } + + // credentials provider + Map credentials = Map.of(); + if (sslConfig.certificate.credentialsProvider.isPresent()) { + String beanName = sslConfig.certificate.credentialsProviderName.orElse(null); + CredentialsProvider credentialsProvider = CredentialsProviderFinder.find(beanName); + String name = sslConfig.certificate.credentialsProvider.get(); + credentials = credentialsProvider.getCredentials(name); + } + final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; + final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials, + sslConfig.certificate.keyStorePasswordKey); + final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials, + sslConfig.certificate.keyStoreKeyPasswordKey); + final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; + final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials, + sslConfig.certificate.trustStorePasswordKey); + final HttpServerOptions serverOptions = new HttpServerOptions(); + + //ssl + if (JdkSSLEngineOptions.isAlpnAvailable()) { + serverOptions.setUseAlpn(true); + serverOptions.setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); + } + int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); + serverOptions.setIdleTimeout(idleTimeout); + serverOptions.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + + if (!certificates.isEmpty() && !keys.isEmpty()) { + createPemKeyCertOptions(certificates, keys, serverOptions); + } else if (keyStoreFile.isPresent()) { + KeyStoreOptions options = createKeyStoreOptions( + keyStoreFile.get(), + keyStorePassword.orElse("password"), + sslConfig.certificate.keyStoreFileType, + sslConfig.certificate.keyStoreProvider, + sslConfig.certificate.keyStoreKeyAlias, + keyStoreKeyPassword); + serverOptions.setKeyCertOptions(options); + } + + if (trustStoreFile.isPresent()) { + if (!trustStorePassword.isPresent()) { + throw new IllegalArgumentException("No trust store password provided"); + } + KeyStoreOptions options = createKeyStoreOptions( + trustStoreFile.get(), + trustStorePassword.get(), + sslConfig.certificate.trustStoreFileType, + sslConfig.certificate.trustStoreProvider, + sslConfig.certificate.trustStoreCertAlias, + Optional.empty()); + serverOptions.setTrustOptions(options); + } + + for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) { + serverOptions.addEnabledCipherSuite(cipher); + } + + for (String protocol : sslConfig.protocols) { + if (!protocol.isEmpty()) { + serverOptions.addEnabledSecureTransportProtocol(protocol); + } + } + serverOptions.setSsl(true); + serverOptions.setSni(sslConfig.sni); + int sslPort = httpConfiguration.determinePort(launchMode); + // -2 instead of -1 (see http) to have vert.x assign two different random ports if both http and https shall be random + serverOptions.setPort(sslPort == 0 ? -2 : sslPort); + serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); + + applyCommonOptionsForManagementInterface(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); + + return serverOptions; + } + + private static Optional getCredential(Optional password, Map credentials, + Optional passwordKey) { + if (password.isPresent()) { + return password; + } + + if (passwordKey.isPresent()) { + return Optional.ofNullable(credentials.get(passwordKey.get())); + } else { + return Optional.empty(); + } + } + + public static void applyCommonOptions(HttpServerOptions httpServerOptions, + HttpBuildTimeConfig buildTimeConfig, + HttpConfiguration httpConfiguration, + List websocketSubProtocols) { + httpServerOptions.setHost(httpConfiguration.host); + setIdleTimeout(httpConfiguration, httpServerOptions); + httpServerOptions.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); + httpServerOptions.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + httpServerOptions.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); + httpServerOptions.setWebSocketSubProtocols(websocketSubProtocols); + httpServerOptions.setReusePort(httpConfiguration.soReusePort); + httpServerOptions.setTcpQuickAck(httpConfiguration.tcpQuickAck); + httpServerOptions.setTcpCork(httpConfiguration.tcpCork); + httpServerOptions.setAcceptBacklog(httpConfiguration.acceptBacklog); + httpServerOptions.setTcpFastOpen(httpConfiguration.tcpFastOpen); + httpServerOptions.setCompressionSupported(buildTimeConfig.enableCompression); + if (buildTimeConfig.compressionLevel.isPresent()) { + httpServerOptions.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); + } + httpServerOptions.setDecompressionSupported(buildTimeConfig.enableDecompression); + httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); + httpServerOptions.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); + } + + public static void applyCommonOptionsForManagementInterface(HttpServerOptions options, + ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration httpConfiguration, + List websocketSubProtocols) { + options.setHost(httpConfiguration.host.orElse("0.0.0.0")); + + int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); + options.setIdleTimeout(idleTimeout); + options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + + options.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); + options.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + options.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); + options.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); + options.setWebSocketSubProtocols(websocketSubProtocols); + options.setAcceptBacklog(httpConfiguration.acceptBacklog); + options.setCompressionSupported(buildTimeConfig.enableCompression); + if (buildTimeConfig.compressionLevel.isPresent()) { + options.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); + } + options.setDecompressionSupported(buildTimeConfig.enableDecompression); + options.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); + } + + private static KeyStoreOptions createKeyStoreOptions(Path path, String password, Optional fileType, + Optional provider, Optional alias, Optional aliasPassword) throws IOException { + final String type; + if (fileType.isPresent()) { + type = fileType.get().toLowerCase(); + } else { + type = findKeystoreFileType(path); + } + + byte[] data = getFileContent(path); + KeyStoreOptions options = new KeyStoreOptions() + .setPassword(password) + .setValue(Buffer.buffer(data)) + .setType(type.toUpperCase()) + .setProvider(provider.orElse(null)) + .setAlias(alias.orElse(null)) + .setAliasPassword(aliasPassword.orElse(null)); + return options; + } + + private static byte[] getFileContent(Path path) throws IOException { + byte[] data; + final InputStream resource = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(ClassPathUtils.toResourceName(path)); + if (resource != null) { + try (InputStream is = resource) { + data = doRead(is); + } + } else { + try (InputStream is = Files.newInputStream(path)) { + data = doRead(is); + } + } + return data; + } + + private static void createPemKeyCertOptions(List certFile, List keyFile, + HttpServerOptions serverOptions) throws IOException { + + if (certFile.size() != keyFile.size()) { + throw new ConfigurationException("Invalid certificate configuration - `files` and `keyFiles` must have the " + + "same number of elements"); + } + + List certificates = new ArrayList<>(); + List keys = new ArrayList<>(); + + for (Path p : certFile) { + final byte[] cert = getFileContent(p); + certificates.add(Buffer.buffer(cert)); + } + + for (Path p : keyFile) { + final byte[] key = getFileContent(p); + keys.add(Buffer.buffer(key)); + } + + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setCertValues(certificates) + .setKeyValues(keys); + serverOptions.setPemKeyCertOptions(pemKeyCertOptions); + } + + private static String findKeystoreFileType(Path storePath) { + final String pathName = storePath.toString(); + if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) { + return "pkcs12"; + } else { + // assume jks + return "jks"; + } + } + + private static byte[] doRead(InputStream is) throws IOException { + return is.readAllBytes(); + } + + private static void setIdleTimeout(HttpConfiguration httpConfiguration, HttpServerOptions options) { + int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); + options.setIdleTimeout(idleTimeout); + options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + } +} diff --git a/integration-tests/management-interface/pom.xml b/integration-tests/management-interface/pom.xml new file mode 100644 index 00000000000000..15c9ab9fd250d0 --- /dev/null +++ b/integration-tests/management-interface/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-management-interface + Quarkus - Integration Tests - Management Interface + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-micrometer-registry-prometheus-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-health-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + diff --git a/integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java b/integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java new file mode 100644 index 00000000000000..dd83f77b59f523 --- /dev/null +++ b/integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java @@ -0,0 +1,13 @@ +package io.quarkus.it.management; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/hello") +public class GreetingResource { + + @GET + public String hello() { + return "hello"; + } +} diff --git a/integration-tests/management-interface/src/main/resources/application.properties b/integration-tests/management-interface/src/main/resources/application.properties new file mode 100644 index 00000000000000..882657a8ebb1ed --- /dev/null +++ b/integration-tests/management-interface/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.management.enabled=true \ No newline at end of file diff --git a/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java new file mode 100644 index 00000000000000..66760f8e080964 --- /dev/null +++ b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java @@ -0,0 +1,12 @@ +package io.quarkus.it.management; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ManagementInterfaceIT extends ManagementInterfaceTestCase { + + @Override + protected String getPrefix() { + return "http://localhost:9000"; // ITs run in prod mode. + } +} diff --git a/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java new file mode 100644 index 00000000000000..354ca8d0633f7d --- /dev/null +++ b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java @@ -0,0 +1,33 @@ +package io.quarkus.it.management; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class ManagementInterfaceTestCase { + + protected String getPrefix() { + return "http://localhost:9001"; + } + + @Test + void verifyThatHealthChecksAreExposedOnManagementInterface() { + RestAssured.get(getPrefix() + "/q/health") + .then().statusCode(200); + + RestAssured.get("/q/health") + .then().statusCode(404); + } + + @Test + void verifyThatMetricsAreExposedOnManagementInterface() { + RestAssured.get(getPrefix() + "/q/metrics") + .then().statusCode(200); + + RestAssured.get("/q/metrics") + .then().statusCode(404); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 93fa09cb595d18..35fff8a2b87a9f 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -346,6 +346,7 @@ google-cloud-functions-http google-cloud-functions istio + management-interface