cookieMaxAge;
+
+ /**
+ * Configuration of sessions stored in memory.
+ */
+ public SessionsInMemoryConfig inMemory;
+
+ /**
+ * Configuration of sessions stored in Redis.
+ */
+ public SessionsRedisConfig redis;
+
+ /**
+ * Configuration of sessions stored in remote Infinispan cache.
+ */
+ public SessionsInfinispanConfig infinispan;
+
+ public enum SessionCookieSecure {
+ /**
+ * The session cookie only has the {@code Secure} attribute when {@code quarkus.http.insecure-requests}
+ * is {@code redirect} or {@code disabled}. If {@code insecure-requests} is {@code enabled}, the session cookie
+ * does not have the {@code Secure} attribute.
+ */
+ AUTO,
+ /**
+ * The session cookie always has the {@code Secure} attribute.
+ */
+ ALWAYS,
+ /**
+ * The session cookie never has the {@code Secure} attribute.
+ */
+ NEVER;
+
+ boolean isEnabled(HttpConfiguration.InsecureRequests insecureRequests) {
+ if (this == ALWAYS) {
+ return true;
+ } else if (this == NEVER) {
+ return false;
+ } else {
+ return insecureRequests != HttpConfiguration.InsecureRequests.ENABLED;
+ }
+ }
+ }
+
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java
new file mode 100644
index 0000000000000..7985e4ffcb818
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java
@@ -0,0 +1,36 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.time.Duration;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration of Vert.x Web sessions stored in memory.
+ */
+@ConfigGroup
+public class SessionsInMemoryConfig {
+ /**
+ * Name of the Vert.x local map or cluster-wide map to store the session data.
+ */
+ @ConfigItem(defaultValue = "vertx-web.sessions")
+ public String mapName;
+
+ /**
+ * Whether in-memory sessions are clustered.
+ *
+ * Ignored when Vert.x clustering is not enabled.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean clustered;
+
+ /**
+ * Maximum time to retry when retrieving session data from the cluster-wide map.
+ * The Vert.x session handler retries when the session data are not found, because
+ * distributing data across the cluster may take time.
+ *
+ * Ignored when in-memory sessions are not clustered.
+ */
+ @ConfigItem(defaultValue = "5s")
+ public Duration retryTimeout;
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanBuildTimeConfig.java
new file mode 100644
index 0000000000000..ad71048fa7a7c
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanBuildTimeConfig.java
@@ -0,0 +1,22 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration of Vert.x Web sessions stored in remote Infinispan cache.
+ */
+@ConfigGroup
+public class SessionsInfinispanBuildTimeConfig {
+ /**
+ * Name of the Infinispan client configured in the Quarkus Infinispan Client extension configuration.
+ * If not set, uses the default (unnamed) Infinispan client.
+ *
+ * Note that the Infinispan client must be configured to so that the user has necessary permissions
+ * on the Infinispan server. The required minimum is the Infinispan {@code deployer} role.
+ */
+ @ConfigItem
+ public Optional clientName;
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanConfig.java
new file mode 100644
index 0000000000000..ef515260fd55f
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanConfig.java
@@ -0,0 +1,27 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.time.Duration;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration of Vert.x Web sessions stored in remote Infinispan cache.
+ */
+@ConfigGroup
+public class SessionsInfinispanConfig {
+ /**
+ * Name of the Infinispan cache used to store session data. If it does not exist, it is created
+ * automatically from Infinispan's default template {@code DIST_SYNC}.
+ */
+ @ConfigItem(defaultValue = "vertx-web.sessions")
+ public String cacheName;
+
+ /**
+ * Maximum time to retry when retrieving session data from the Infinispan cache.
+ * The Vert.x session handler retries when the session data are not found, because
+ * distributing data across an Infinispan cluster may take time.
+ */
+ @ConfigItem(defaultValue = "5s")
+ public Duration retryTimeout;
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisBuildTimeConfig.java
new file mode 100644
index 0000000000000..e7a2b7f3150d5
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisBuildTimeConfig.java
@@ -0,0 +1,19 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration of Vert.x Web sessions stored in Redis.
+ */
+@ConfigGroup
+public class SessionsRedisBuildTimeConfig {
+ /**
+ * Name of the Redis client configured in the Quarkus Redis extension configuration.
+ * If not set, uses the default (unnamed) Redis client.
+ */
+ @ConfigItem
+ public Optional clientName;
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisConfig.java
new file mode 100644
index 0000000000000..38314ce1f3bb3
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisConfig.java
@@ -0,0 +1,20 @@
+package io.quarkus.vertx.http.runtime;
+
+import java.time.Duration;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration of Vert.x Web sessions stored in Redis.
+ */
+@ConfigGroup
+public class SessionsRedisConfig {
+ /**
+ * Maximum time to retry when retrieving session data from the Redis server.
+ * The Vert.x session handler retries when the session data are not found, because
+ * distributing data across a potential Redis cluster may take some time.
+ */
+ @ConfigItem(defaultValue = "2s")
+ public Duration retryTimeout;
+}
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 fbcb893f48b9d..b65ecbb949993 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
@@ -7,9 +7,11 @@
import java.net.BindException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -74,6 +76,7 @@
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.quarkus.vertx.http.sessions.spi.SessionStoreProvider;
import io.smallrye.common.vertx.VertxContext;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
@@ -104,6 +107,10 @@
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CorsHandler;
+import io.vertx.ext.web.handler.SessionHandler;
+import io.vertx.ext.web.sstore.ClusteredSessionStore;
+import io.vertx.ext.web.sstore.LocalSessionStore;
+import io.vertx.ext.web.sstore.SessionStore;
@Recorder
public class VertxHttpRecorder {
@@ -191,14 +198,18 @@ private boolean uriValid(HttpServerRequest httpServerRequest) {
final RuntimeValue managementConfiguration;
private static volatile Handler managementRouter;
+ final RuntimeValue vertxConfiguration;
+
public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig,
ManagementInterfaceBuildTimeConfig managementBuildTimeConfig,
RuntimeValue httpConfiguration,
- RuntimeValue managementConfiguration) {
+ RuntimeValue managementConfiguration,
+ RuntimeValue vertxConfiguration) {
this.httpBuildTimeConfig = httpBuildTimeConfig;
this.httpConfiguration = httpConfiguration;
this.managementBuildTimeConfig = managementBuildTimeConfig;
this.managementConfiguration = managementConfiguration;
+ this.vertxConfiguration = vertxConfiguration;
}
public static void setHotReplacement(Handler handler, HotReplacementContext hrc) {
@@ -346,6 +357,36 @@ public void mountFrameworkRouter(RuntimeValue mainRouter, RuntimeValue createInMemorySessionStore() {
+ Vertx vertx = VertxCoreRecorder.getVertx().get();
+ if (httpConfiguration.getValue().sessions.inMemory.clustered
+ && vertxConfiguration.getValue().cluster() != null
+ && vertxConfiguration.getValue().cluster().clustered()) {
+ return new RuntimeValue<>(ClusteredSessionStore.create(vertx,
+ httpConfiguration.getValue().sessions.inMemory.mapName,
+ httpConfiguration.getValue().sessions.inMemory.retryTimeout.toMillis()));
+ } else {
+ // TODO maybe make reaper interval also configurable?
+ return new RuntimeValue<>(LocalSessionStore.create(vertx,
+ httpConfiguration.getValue().sessions.inMemory.mapName));
+ }
+ }
+
+ public RuntimeValue createRedisSessionStore(SessionStoreProvider provider) {
+ Map config = new HashMap<>();
+ config.put("clientName", httpBuildTimeConfig.sessions.redis.clientName.orElse(null));
+ config.put("retryTimeout", httpConfiguration.getValue().sessions.redis.retryTimeout);
+ return new RuntimeValue<>(provider.create(VertxCoreRecorder.getVertx().get(), config));
+ }
+
+ public RuntimeValue createInfinispanSessionStore(SessionStoreProvider provider) {
+ Map config = new HashMap<>();
+ config.put("clientName", httpBuildTimeConfig.sessions.infinispan.clientName.orElse(null));
+ config.put("cacheName", httpConfiguration.getValue().sessions.infinispan.cacheName);
+ config.put("retryTimeout", httpConfiguration.getValue().sessions.infinispan.retryTimeout);
+ return new RuntimeValue<>(provider.create(VertxCoreRecorder.getVertx().get(), config));
+ }
+
public void finalizeRouter(BeanContainer container, Consumer defaultRouteHandler,
List filterList, List managementInterfaceFilterList, Supplier vertx,
LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue,
@@ -355,7 +396,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute
LaunchMode launchMode, boolean requireBodyHandler,
Handler bodyHandler,
GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig,
- Executor executor) {
+ Executor executor, RuntimeValue sessionStore) {
HttpConfiguration httpConfiguration = this.httpConfiguration.getValue();
// install the default route at the end
Router httpRouteRouter = httpRouterRuntimeValue.getValue();
@@ -413,6 +454,23 @@ public void handle(RoutingContext routingContext) {
// Headers sent on any request, regardless of the response
HttpServerCommonHandlers.applyHeaders(httpConfiguration.header, httpRouteRouter);
+ if (sessionStore != null) {
+ SessionsConfig sessions = httpConfiguration.sessions;
+ // TODO probably need to normalize and relativize the path here
+ String path = sessions.path;
+ SessionHandler sessionHandler = SessionHandler.create(sessionStore.getValue())
+ .setSessionTimeout(sessions.timeout.toMillis())
+ .setMinLength(sessions.idLength)
+ .setSessionCookiePath(path)
+ .setSessionCookieName(sessions.cookieName)
+ .setCookieHttpOnlyFlag(sessions.cookieHttpOnly)
+ .setCookieSecureFlag(sessions.cookieSecure.isEnabled(httpConfiguration.insecureRequests))
+ .setCookieSameSite(sessions.cookieSameSite.orElse(null))
+ .setCookieMaxAge(sessions.cookieMaxAge.map(Duration::toMillis).orElse(-1L));
+ // TODO verify if this is the correct router to which the session handler should be installed
+ httpRouteRouter.route(path).handler(sessionHandler);
+ }
+
Handler root;
if (rootPath.equals("/")) {
if (hotReplacementHandler != null) {
diff --git a/extensions/vertx-http/sessions-spi/pom.xml b/extensions/vertx-http/sessions-spi/pom.xml
new file mode 100644
index 0000000000000..ad9b4b21be033
--- /dev/null
+++ b/extensions/vertx-http/sessions-spi/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ quarkus-vertx-http-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-vertx-http-sessions-spi
+ Quarkus - Vert.x - HTTP - Sessions SPI
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.vertx
+ vertx-web
+
+
+
+
diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreKind.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreKind.java
new file mode 100644
index 0000000000000..087627aa29002
--- /dev/null
+++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreKind.java
@@ -0,0 +1,6 @@
+package io.quarkus.vertx.http.sessions.spi;
+
+public enum SessionStoreKind {
+ REDIS,
+ INFINISPAN,
+}
diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreProvider.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreProvider.java
new file mode 100644
index 0000000000000..9599413b606aa
--- /dev/null
+++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreProvider.java
@@ -0,0 +1,10 @@
+package io.quarkus.vertx.http.sessions.spi;
+
+import java.util.Map;
+
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.sstore.SessionStore;
+
+public interface SessionStoreProvider {
+ SessionStore create(Vertx vertx, Map config);
+}
diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreRequestBuildItem.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreRequestBuildItem.java
new file mode 100644
index 0000000000000..ebde51806fc4f
--- /dev/null
+++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreRequestBuildItem.java
@@ -0,0 +1,24 @@
+package io.quarkus.vertx.http.sessions.spi;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import io.quarkus.builder.item.SimpleBuildItem;
+
+public final class SessionStoreRequestBuildItem extends SimpleBuildItem {
+ private final SessionStoreKind kind;
+ private final Optional clientName; // may be empty for the unnamed client
+
+ public SessionStoreRequestBuildItem(SessionStoreKind kind, Optional clientName) {
+ this.kind = Objects.requireNonNull(kind);
+ this.clientName = Objects.requireNonNull(clientName);
+ }
+
+ public boolean is(SessionStoreKind kind) {
+ return this.kind == kind;
+ }
+
+ public String clientName(String defaultName) {
+ return clientName.orElse(defaultName);
+ }
+}
diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreResponseBuildItem.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreResponseBuildItem.java
new file mode 100644
index 0000000000000..c735c5098fa77
--- /dev/null
+++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreResponseBuildItem.java
@@ -0,0 +1,17 @@
+package io.quarkus.vertx.http.sessions.spi;
+
+import java.util.Objects;
+
+import io.quarkus.builder.item.SimpleBuildItem;
+
+public final class SessionStoreResponseBuildItem extends SimpleBuildItem {
+ private final SessionStoreProvider provider;
+
+ public SessionStoreResponseBuildItem(SessionStoreProvider provider) {
+ this.provider = Objects.requireNonNull(provider);
+ }
+
+ public SessionStoreProvider getProvider() {
+ return provider;
+ }
+}