From 1ea53bbeaf88af0a4f9f2318bc32685791ca936b Mon Sep 17 00:00:00 2001 From: Jono Date: Fri, 22 Dec 2023 01:34:13 +1300 Subject: [PATCH 1/2] CAMEL-20019: demo session handler implementation --- components/camel-platform-http-vertx/pom.xml | 5 + .../http/vertx/VertxPlatformHttpServer.java | 5 + .../VertxPlatformHttpServerConfiguration.java | 78 +++++++ .../vertx/VertxPlatformHttpSessionTest.java | 206 ++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java diff --git a/components/camel-platform-http-vertx/pom.xml b/components/camel-platform-http-vertx/pom.xml index 45a235b7b47c2..cb384edb1a71a 100644 --- a/components/camel-platform-http-vertx/pom.xml +++ b/components/camel-platform-http-vertx/pom.xml @@ -84,6 +84,11 @@ camel-log test + + org.apache.camel + camel-vertx-http + test + io.vertx vertx-auth-properties diff --git a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServer.java b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServer.java index c81e3e8d5b01d..132c3c6139ac8 100644 --- a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServer.java +++ b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServer.java @@ -183,6 +183,11 @@ protected void initializeServer() { subRouter.route().handler(createCorsHandler(configuration)); } + if (configuration.getSessionConfig().isEnabled()) { + subRouter.route().handler( + configuration.getSessionConfig().createSessionHandler(vertx)); + } + router.route(configuration.getPath() + "*").subRouter(subRouter); context.getRegistry().bind( diff --git a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java index 3720b250b59ce..386438f02d5a0 100644 --- a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java +++ b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java @@ -19,6 +19,11 @@ import java.time.Duration; import java.util.List; +import io.vertx.core.Vertx; +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; import org.apache.camel.support.jsse.SSLContextParameters; /** @@ -39,6 +44,7 @@ public class VertxPlatformHttpServerConfiguration { private BodyHandler bodyHandler = new BodyHandler(); private Cors cors = new Cors(); + private SessionConfig sessionConfig = new SessionConfig(); public int getPort() { return getBindPort(); @@ -112,6 +118,14 @@ public void setCors(Cors corsConfiguration) { this.cors = corsConfiguration; } + public SessionConfig getSessionConfig() { + return sessionConfig; + } + + public void setSessionConfig(SessionConfig sessionConfig) { + this.sessionConfig = sessionConfig; + } + public BodyHandler getBodyHandler() { return bodyHandler; } @@ -120,6 +134,70 @@ public void setBodyHandler(BodyHandler bodyHandler) { this.bodyHandler = bodyHandler; } + public static class SessionConfig { + + private boolean enabled; + + private SessionStoreType storeType = SessionStoreType.LOCAL; + + private String cookieName = SessionHandler.DEFAULT_SESSION_COOKIE_NAME; + + private long sessionTimeOut = SessionHandler.DEFAULT_SESSION_TIMEOUT; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setStoreType(SessionStoreType storeType) { + this.storeType = storeType; + } + + public void setCookieName(String cookieName) { + if (cookieName != null) { + this.cookieName = cookieName; + } + } + + public void setTimeout(long timeout) { + this.sessionTimeOut = timeout; + } + + public SessionHandler createSessionHandler(Vertx vertx) { + SessionStore sessionStore = storeType.create(vertx); + SessionHandler handler = SessionHandler.create(sessionStore); + configure(handler); + return handler; + } + + private void configure(SessionHandler handler) { + handler.setSessionTimeout(this.sessionTimeOut) + .setSessionCookieName(this.cookieName); + // TODO and other properties; + } + + // TODO CookieSessionStore also + public enum SessionStoreType { + LOCAL { + @Override + public SessionStore create(Vertx vertx) { + return LocalSessionStore.create(vertx); + } + }, + CLUSTERED { + @Override + public SessionStore create(Vertx vertx) { + return ClusteredSessionStore.create(vertx); + } + }; + + public abstract SessionStore create(Vertx vertx); + } + } + public static class Cors { private boolean enabled; private List origins; diff --git a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java new file mode 100644 index 0000000000000..80e5aa316a4f4 --- /dev/null +++ b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.platform.http.vertx; + +import java.util.ArrayList; +import java.util.List; + +import io.netty.handler.codec.http.cookie.Cookie; +import io.restassured.RestAssured; +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.spi.CookieStore; +import org.apache.camel.*; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.test.AvailablePortFinder; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static io.restassured.matcher.RestAssuredMatchers.detailedCookie; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VertxPlatformHttpSessionTest extends CamelTestSupport { + + @Test + public void testSessionCreation() throws Exception { + Vertx vertx = Vertx.vertx(); + CamelContext context = createCamelContext(configuration -> { + VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig + = new VertxPlatformHttpServerConfiguration.SessionConfig(); + sessionConfig.setEnabled(true); + configuration.setSessionConfig(sessionConfig); + }); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/session") + .routeId("session") + .setBody().constant("session"); + } + }); + + context.getRegistry().bind("vertx", vertx); + + try { + context.start(); + + // returns set-cookie header for created session + String sessionCookieValue = given() + .when() + .get("/session") + .then() + .statusCode(200) + .header("set-cookie", startsWith("vertx-web.session=")) // new vertx-web session created + // details of the set session cookie + .cookie("vertx-web.session", detailedCookie().path("/").value(notNullValue())) + .header("cookie", nullValue()) + .body(equalTo("session")) + .extract().cookie("vertx-web.session"); + + // pass session cookie back on subsequent call + given() + .header("cookie", "vertx-web.session=" + sessionCookieValue) + .when() + .get("/session") + .then() + .statusCode(200) + .header("set-cookie", nullValue()) // session already established. + .header("cookie", "vertx-web.session=" + sessionCookieValue) + .body(equalTo("session")); + + } finally { + context.stop(); + vertx.close(); + } + } + + @Test + public void testNewSession() throws Exception { + int port = AvailablePortFinder.getNextAvailable(); + + final CamelContext context = createCamelContext(configuration -> { + VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig + = new VertxPlatformHttpServerConfiguration.SessionConfig(); + sessionConfig.setEnabled(true); + + configuration.setSessionConfig(sessionConfig); + }, port); + + CookieStore customCookieStore = new CustomCookieStore(); + context.getRegistry().bind("customCookieStore", customCookieStore); + + try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/session") + .routeId("session") + .setBody().constant("session"); + + from("direct:session") + .toF("vertx-http:http://localhost:%d/session?sessionManagement=true&cookieStore=#customCookieStore", + port); + } + }); + + context.start(); + + ProducerTemplate template = context.createProducerTemplate(); + Exchange exchange = template.request("direct:session", ex -> { + }); + + // session created by session handler + String sessionCookie = (String) exchange.getMessage().getHeader("set-cookie"); + assertNotNull(sessionCookie); + assertTrue(sessionCookie.startsWith("vertx-web.session=")); + + // returned session saved in vertx-http cookie store + Cookie savedCookie = customCookieStore.get(false, null, null).iterator().next(); + assertNotNull(savedCookie); + assertEquals("vertx-web.session", savedCookie.name()); + assertTrue(sessionCookie.startsWith(savedCookie.name() + '=' + savedCookie.value())); + + // call again, headers added from cookie store + exchange = template.request("direct:session", ex -> { + }); + // cookie for established session + Object cookieHeader = exchange.getMessage().getHeader("cookie"); + assertNotNull(cookieHeader); + assert ((savedCookie.name() + '=' + savedCookie.value()).equals(cookieHeader)); + // session already established, no new cookie set + sessionCookie = (String) exchange.getMessage().getHeader("set-cookie"); + assertNull(sessionCookie); + } finally { + context.stop(); + } + } + + static CamelContext createCamelContext(ServerConfigurationCustomizer customizer) + throws Exception { + return createCamelContext(customizer, AvailablePortFinder.getNextAvailable()); + } + + static CamelContext createCamelContext(ServerConfigurationCustomizer customizer, int bindPort) + throws Exception { + VertxPlatformHttpServerConfiguration conf = new VertxPlatformHttpServerConfiguration(); + conf.setBindPort(bindPort); + + RestAssured.port = bindPort; + + if (customizer != null) { + customizer.customize(conf); + } + + CamelContext context = new DefaultCamelContext(); + context.addService(new VertxPlatformHttpServer(conf)); + return context; + } + + interface ServerConfigurationCustomizer { + void customize(VertxPlatformHttpServerConfiguration configuration); + } + + // Cookie store for the producer + private static final class CustomCookieStore implements CookieStore { + private final List cookies = new ArrayList<>(); + + @Override + public Iterable get(Boolean ssl, String domain, String path) { + return cookies; + } + + @Override + public CookieStore put(io.netty.handler.codec.http.cookie.Cookie cookie) { + cookies.add(cookie); + return this; + } + + @Override + public CookieStore remove(io.netty.handler.codec.http.cookie.Cookie cookie) { + cookies.remove(cookie); + return this; + } + } +} From a64af8b65c86254b23fee5bb3e8bd38cb56efb39 Mon Sep 17 00:00:00 2001 From: Jono Date: Mon, 25 Dec 2023 01:53:57 +1300 Subject: [PATCH 2/2] CAMEL-20019: updates in response to review comments --- components/camel-platform-http-vertx/pom.xml | 5 - .../VertxPlatformHttpServerConfiguration.java | 119 ++++++--- .../vertx/VertxPlatformHttpSessionTest.java | 248 +++++++++++------- .../pages/camel-4x-upgrade-guide-4_4.adoc | 10 + 4 files changed, 248 insertions(+), 134 deletions(-) diff --git a/components/camel-platform-http-vertx/pom.xml b/components/camel-platform-http-vertx/pom.xml index cb384edb1a71a..45a235b7b47c2 100644 --- a/components/camel-platform-http-vertx/pom.xml +++ b/components/camel-platform-http-vertx/pom.xml @@ -84,11 +84,6 @@ camel-log test - - org.apache.camel - camel-vertx-http - test - io.vertx vertx-auth-properties diff --git a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java index 386438f02d5a0..b2bcc078e4e01 100644 --- a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java +++ b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerConfiguration.java @@ -20,6 +20,7 @@ import java.util.List; import io.vertx.core.Vertx; +import io.vertx.core.http.CookieSameSite; import io.vertx.ext.web.handler.SessionHandler; import io.vertx.ext.web.sstore.ClusteredSessionStore; import io.vertx.ext.web.sstore.LocalSessionStore; @@ -135,14 +136,15 @@ public void setBodyHandler(BodyHandler bodyHandler) { } public static class SessionConfig { - private boolean enabled; - private SessionStoreType storeType = SessionStoreType.LOCAL; - - private String cookieName = SessionHandler.DEFAULT_SESSION_COOKIE_NAME; - + private String sessionCookieName = SessionHandler.DEFAULT_SESSION_COOKIE_NAME; + private String sessionCookiePath = SessionHandler.DEFAULT_SESSION_COOKIE_PATH; private long sessionTimeOut = SessionHandler.DEFAULT_SESSION_TIMEOUT; + private boolean cookieSecure = SessionHandler.DEFAULT_COOKIE_SECURE_FLAG; + private boolean cookieHttpOnly = SessionHandler.DEFAULT_COOKIE_HTTP_ONLY_FLAG; + private int sessionIdMinLength = SessionHandler.DEFAULT_SESSIONID_MIN_LENGTH; + private CookieSameSite cookieSameSite = CookieSameSite.STRICT; public boolean isEnabled() { return enabled; @@ -152,20 +154,73 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + public SessionStoreType getStoreType() { + return this.storeType; + } + public void setStoreType(SessionStoreType storeType) { this.storeType = storeType; } - public void setCookieName(String cookieName) { - if (cookieName != null) { - this.cookieName = cookieName; - } + public String getSessionCookieName() { + return this.sessionCookieName; + } + + public void setSessionCookieName(String sessionCookieName) { + this.sessionCookieName = sessionCookieName; } - public void setTimeout(long timeout) { + public String getSessionCookiePath() { + return this.sessionCookiePath; + } + + public void setSessionCookiePath(String sessionCookiePath) { + this.sessionCookiePath = sessionCookiePath; + } + + public long getSessionTimeOut() { + return this.sessionTimeOut; + } + + public void setSessionTimeout(long timeout) { this.sessionTimeOut = timeout; } + public boolean isCookieSecure() { + return this.cookieSecure; + } + + // Instructs browsers to only send the cookie over HTTPS when set. + public void setCookieSecure(boolean cookieSecure) { + this.cookieSecure = cookieSecure; + } + + public boolean isCookieHttpOnly() { + return this.cookieHttpOnly; + } + + // Instructs browsers to prevent Javascript access to the cookie. + // Defends against XSS attacks. + public void setCookieHttpOnly(boolean cookieHttpOnly) { + this.cookieHttpOnly = cookieHttpOnly; + } + + public int getSessionIdMinLength() { + return this.sessionIdMinLength; + } + + public void setSessionIdMinLength(int sessionIdMinLength) { + this.sessionIdMinLength = sessionIdMinLength; + } + + public CookieSameSite getCookieSameSite() { + return this.cookieSameSite; + } + + public void setCookieSameSite(CookieSameSite cookieSameSite) { + this.cookieSameSite = cookieSameSite; + } + public SessionHandler createSessionHandler(Vertx vertx) { SessionStore sessionStore = storeType.create(vertx); SessionHandler handler = SessionHandler.create(sessionStore); @@ -175,29 +230,33 @@ public SessionHandler createSessionHandler(Vertx vertx) { private void configure(SessionHandler handler) { handler.setSessionTimeout(this.sessionTimeOut) - .setSessionCookieName(this.cookieName); - // TODO and other properties; - } - - // TODO CookieSessionStore also - public enum SessionStoreType { - LOCAL { - @Override - public SessionStore create(Vertx vertx) { - return LocalSessionStore.create(vertx); - } - }, - CLUSTERED { - @Override - public SessionStore create(Vertx vertx) { - return ClusteredSessionStore.create(vertx); - } - }; - - public abstract SessionStore create(Vertx vertx); + .setSessionCookieName(this.sessionCookieName) + .setSessionCookiePath(this.sessionCookiePath) + .setSessionTimeout(this.sessionTimeOut) + .setCookieHttpOnlyFlag(this.cookieHttpOnly) + .setCookieSecureFlag(this.cookieSecure) + .setMinLength(this.sessionIdMinLength) + .setCookieSameSite(this.cookieSameSite); } } + public enum SessionStoreType { + LOCAL { + @Override + public SessionStore create(Vertx vertx) { + return LocalSessionStore.create(vertx); + } + }, + CLUSTERED { + @Override + public SessionStore create(Vertx vertx) { + return ClusteredSessionStore.create(vertx); + } + }; + + public abstract SessionStore create(Vertx vertx); + } + public static class Cors { private boolean enabled; private List origins; diff --git a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java index 80e5aa316a4f4..7ef4d3d8862cb 100644 --- a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java +++ b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java @@ -16,18 +16,23 @@ */ package org.apache.camel.component.platform.http.vertx; -import java.util.ArrayList; -import java.util.List; +import java.util.Arrays; -import io.netty.handler.codec.http.cookie.Cookie; import io.restassured.RestAssured; -import io.vertx.core.Vertx; -import io.vertx.ext.web.client.spi.CookieStore; -import org.apache.camel.*; +import io.vertx.core.Handler; +import io.vertx.core.http.CookieSameSite; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.Session; +import io.vertx.ext.web.handler.SessionHandler; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.ProducerTemplate; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.platform.http.PlatformHttpComponent; +import org.apache.camel.component.platform.http.PlatformHttpConstants; +import org.apache.camel.http.base.cookie.InstanceCookieHandler; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.test.AvailablePortFinder; -import org.apache.camel.test.junit5.CamelTestSupport; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; @@ -35,172 +40,217 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; -public class VertxPlatformHttpSessionTest extends CamelTestSupport { +public class VertxPlatformHttpSessionTest { @Test - public void testSessionCreation() throws Exception { - Vertx vertx = Vertx.vertx(); - CamelContext context = createCamelContext(configuration -> { - VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig - = new VertxPlatformHttpServerConfiguration.SessionConfig(); + public void testSessionDisabled() throws Exception { + CamelContext context = createCamelContext(sessionConfig -> { + // session handling disabled by default + }); + + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/disabled") + .setBody().constant("disabled"); + } + }); + + try { + context.start(); + + given() + .when() + .get("/disabled") + .then() + .statusCode(200) + .header("set-cookie", nullValue()) + .header("cookie", nullValue()) + .body(equalTo("disabled")); + } finally { + context.stop(); + } + } + + @Test + public void testCookeForDefaultSessionConfig() throws Exception { + CamelContext context = createCamelContext(sessionConfig -> { sessionConfig.setEnabled(true); - configuration.setSessionConfig(sessionConfig); }); + context.addRoutes(new RouteBuilder() { @Override public void configure() { from("platform-http:/session") - .routeId("session") .setBody().constant("session"); } }); - context.getRegistry().bind("vertx", vertx); - try { context.start(); - // returns set-cookie header for created session String sessionCookieValue = given() .when() .get("/session") .then() .statusCode(200) - .header("set-cookie", startsWith("vertx-web.session=")) // new vertx-web session created - // details of the set session cookie - .cookie("vertx-web.session", detailedCookie().path("/").value(notNullValue())) + .cookie("vertx-web.session", + detailedCookie() + .path("/").value(notNullValue()) + .httpOnly(false) + .secured(false) + .sameSite("Strict")) .header("cookie", nullValue()) .body(equalTo("session")) .extract().cookie("vertx-web.session"); - // pass session cookie back on subsequent call - given() - .header("cookie", "vertx-web.session=" + sessionCookieValue) + assertTrue(sessionCookieValue.length() >= SessionHandler.DEFAULT_SESSIONID_MIN_LENGTH); + + } finally { + context.stop(); + } + } + + @Test + public void testCookieForModifiedSessionConfig() throws Exception { + CamelContext context = createCamelContext(sessionConfig -> { + sessionConfig.setSessionCookieName("vertx-session"); + sessionConfig.setEnabled(true); + sessionConfig.setSessionCookiePath("/session"); + sessionConfig.setCookieSecure(true); + sessionConfig.setCookieHttpOnly(true); + sessionConfig.setCookieSameSite(CookieSameSite.LAX); + sessionConfig.setSessionIdMinLength(64); + }); + + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/session") + .setBody().constant("session"); + } + }); + + try { + context.start(); + + String sessionCookieValue = given() .when() .get("/session") .then() .statusCode(200) - .header("set-cookie", nullValue()) // session already established. - .header("cookie", "vertx-web.session=" + sessionCookieValue) - .body(equalTo("session")); + .cookie("vertx-session", + detailedCookie() + .path("/session").value(notNullValue()) + .httpOnly(true) + .secured(true) + .sameSite("Lax")) + .header("cookie", nullValue()) + .body(equalTo("session")) + .extract().cookie("vertx-session"); + + assertTrue(sessionCookieValue.length() >= 64); } finally { context.stop(); - vertx.close(); } } @Test - public void testNewSession() throws Exception { + public void testSessionHandling() throws Exception { int port = AvailablePortFinder.getNextAvailable(); - - final CamelContext context = createCamelContext(configuration -> { - VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig - = new VertxPlatformHttpServerConfiguration.SessionConfig(); - sessionConfig.setEnabled(true); - - configuration.setSessionConfig(sessionConfig); - }, port); - - CookieStore customCookieStore = new CustomCookieStore(); - context.getRegistry().bind("customCookieStore", customCookieStore); + CamelContext context = createCamelContext(port, + sessionConfig -> { + sessionConfig.setEnabled(true); + }); + addPlatformHttpEngineHandler(context, new HitCountHandler()); + context.getRegistry().bind("instanceCookieHander", new InstanceCookieHandler()); try { context.addRoutes(new RouteBuilder() { @Override public void configure() { from("platform-http:/session") - .routeId("session") .setBody().constant("session"); from("direct:session") - .toF("vertx-http:http://localhost:%d/session?sessionManagement=true&cookieStore=#customCookieStore", + .toF("http://localhost:%d/session?cookieHandler=#instanceCookieHander", port); } }); context.start(); + // initial call establishes session ProducerTemplate template = context.createProducerTemplate(); - Exchange exchange = template.request("direct:session", ex -> { - }); - - // session created by session handler - String sessionCookie = (String) exchange.getMessage().getHeader("set-cookie"); - assertNotNull(sessionCookie); - assertTrue(sessionCookie.startsWith("vertx-web.session=")); + Exchange exchange = template.request("direct:session", null); + // 'set-cookie' header for new session, e.g. 'vertx-web.session=735944d69685aaf63421fb5b3c116b84; Path=/; SameSite=Strict' + String sessionCookie = getHeader("set-cookie", exchange); + assertNotNull(getHeader("set-cookie", exchange)); + assertEquals(getHeader("hitcount", exchange), "1"); - // returned session saved in vertx-http cookie store - Cookie savedCookie = customCookieStore.get(false, null, null).iterator().next(); - assertNotNull(savedCookie); - assertEquals("vertx-web.session", savedCookie.name()); - assertTrue(sessionCookie.startsWith(savedCookie.name() + '=' + savedCookie.value())); + // subsequent call reuses session + exchange = template.request("direct:session", null); + // 'cookie' header for existing session, e.g. 'vertx-web.session=735944d69685aaf63421fb5b3c116b84' + String cookieHeader = getHeader("cookie", exchange); + assertEquals(cookieHeader, sessionCookie.substring(0, sessionCookie.indexOf(';'))); + assertNull(getHeader("set-cookie", exchange)); + assertEquals(getHeader("hitcount", exchange), "2"); - // call again, headers added from cookie store - exchange = template.request("direct:session", ex -> { - }); - // cookie for established session - Object cookieHeader = exchange.getMessage().getHeader("cookie"); - assertNotNull(cookieHeader); - assert ((savedCookie.name() + '=' + savedCookie.value()).equals(cookieHeader)); - // session already established, no new cookie set - sessionCookie = (String) exchange.getMessage().getHeader("set-cookie"); - assertNull(sessionCookie); } finally { context.stop(); } } - static CamelContext createCamelContext(ServerConfigurationCustomizer customizer) + private String getHeader(String header, Exchange exchange) { + return (String) exchange.getMessage().getHeader(header); + } + + private CamelContext createCamelContext(SessionConfigCustomizer customizer) throws Exception { - return createCamelContext(customizer, AvailablePortFinder.getNextAvailable()); + int bindPort = AvailablePortFinder.getNextAvailable(); + RestAssured.port = bindPort; + return createCamelContext(bindPort, customizer); } - static CamelContext createCamelContext(ServerConfigurationCustomizer customizer, int bindPort) + private CamelContext createCamelContext(int bindPort, SessionConfigCustomizer customizer) throws Exception { VertxPlatformHttpServerConfiguration conf = new VertxPlatformHttpServerConfiguration(); conf.setBindPort(bindPort); - RestAssured.port = bindPort; + VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig + = new VertxPlatformHttpServerConfiguration.SessionConfig(); + customizer.customize(sessionConfig); + conf.setSessionConfig(sessionConfig); - if (customizer != null) { - customizer.customize(conf); - } - - CamelContext context = new DefaultCamelContext(); - context.addService(new VertxPlatformHttpServer(conf)); - return context; + CamelContext camelContext = new DefaultCamelContext(); + camelContext.addService(new VertxPlatformHttpServer(conf)); + return camelContext; } - interface ServerConfigurationCustomizer { - void customize(VertxPlatformHttpServerConfiguration configuration); + private void addPlatformHttpEngineHandler(CamelContext camelContext, Handler handler) { + VertxPlatformHttpEngine platformEngine = new VertxPlatformHttpEngine(); + platformEngine.setHandlers(Arrays.asList(handler)); + PlatformHttpComponent component = new PlatformHttpComponent(camelContext); + component.setEngine(platformEngine); + camelContext.getRegistry().bind(PlatformHttpConstants.PLATFORM_HTTP_COMPONENT_NAME, component); } - // Cookie store for the producer - private static final class CustomCookieStore implements CookieStore { - private final List cookies = new ArrayList<>(); - - @Override - public Iterable get(Boolean ssl, String domain, String path) { - return cookies; - } - + private class HitCountHandler implements Handler { @Override - public CookieStore put(io.netty.handler.codec.http.cookie.Cookie cookie) { - cookies.add(cookie); - return this; + public void handle(RoutingContext routingContext) { + Session session = routingContext.session(); + Integer cnt = session.get("hitcount"); + cnt = (cnt == null ? 0 : cnt) + 1; + session.put("hitcount", cnt); + routingContext.response().putHeader("hitcount", Integer.toString(cnt)); + routingContext.next(); } + } - @Override - public CookieStore remove(io.netty.handler.codec.http.cookie.Cookie cookie) { - cookies.remove(cookie); - return this; - } + interface SessionConfigCustomizer { + void customize(VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig); } } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_4.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_4.adoc index 872b1a331b4d0..2d25bf0f26739 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_4.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_4.adoc @@ -87,3 +87,13 @@ The route controller configuration has been moved from general main to its own g All keys started with `camel.springboot.routesController` should be renamed to `camel.routecontroller.`, for example `camel.springboot.routeControllerBackOffDelay` should be renamed to `camel.routecontroller.backOffDelay`. And the option `camel.springboot.routeControllerSuperviseEnabled` has been renamed to `camel.routecontroller.enabled`. + +=== camel-platform-http-vertx + +Added configuration to enable Vert.x session handling. +Sessions are disabled by default, but can be enabled by setting the `enabled` property on `VertxPlatformHttpServerConfiguration.SessionConfig` +to `true`. +Other properties include `sessionCookieName`, `sessionCookiePath`, `sessionTimeout`, `cookieSecure`, `cookieHttpOnly` +`cookieSameSite` and `storeType`. +The session `storeType` defaults to the Vert.x `LocalSessionStore` and `cookieSameSite` to `Strict`. The remainder +of the properties are configured with Vert.x defaults if not set.