You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by jo...@apache.org on 2023/12/27 09:18:51 UTC

(camel) branch main updated: CAMEL-20019: camel-platform-http-vertx implement session handler (#12530)

This is an automated email from the ASF dual-hosted git repository.

jono pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new f79496b51dc CAMEL-20019: camel-platform-http-vertx implement session handler (#12530)
f79496b51dc is described below

commit f79496b51dc8278950b641f4f07ad140de3b375a
Author: Jono Morris <jo...@apache.org>
AuthorDate: Wed Dec 27 22:18:45 2023 +1300

    CAMEL-20019: camel-platform-http-vertx implement session handler (#12530)
    
    * CAMEL-20019: demo session handler implementation
    
    * CAMEL-20019: updates in response to review comments
---
 .../http/vertx/VertxPlatformHttpServer.java        |   5 +
 .../VertxPlatformHttpServerConfiguration.java      | 137 +++++++++++
 .../http/vertx/VertxPlatformHttpSessionTest.java   | 256 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_4.adoc     |  10 +
 4 files changed, 408 insertions(+)

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 c81e3e8d5b0..132c3c6139a 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 @@ public class VertxPlatformHttpServer extends ServiceSupport implements CamelCont
             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 3720b250b59..b2bcc078e4e 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,12 @@ package org.apache.camel.component.platform.http.vertx;
 import java.time.Duration;
 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;
+import io.vertx.ext.web.sstore.SessionStore;
 import org.apache.camel.support.jsse.SSLContextParameters;
 
 /**
@@ -39,6 +45,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 +119,14 @@ public class VertxPlatformHttpServerConfiguration {
         this.cors = corsConfiguration;
     }
 
+    public SessionConfig getSessionConfig() {
+        return sessionConfig;
+    }
+
+    public void setSessionConfig(SessionConfig sessionConfig) {
+        this.sessionConfig = sessionConfig;
+    }
+
     public BodyHandler getBodyHandler() {
         return bodyHandler;
     }
@@ -120,6 +135,128 @@ public class VertxPlatformHttpServerConfiguration {
         this.bodyHandler = bodyHandler;
     }
 
+    public static class SessionConfig {
+        private boolean enabled;
+        private SessionStoreType storeType = SessionStoreType.LOCAL;
+        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;
+        }
+
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+
+        public SessionStoreType getStoreType() {
+            return this.storeType;
+        }
+
+        public void setStoreType(SessionStoreType storeType) {
+            this.storeType = storeType;
+        }
+
+        public String getSessionCookieName() {
+            return this.sessionCookieName;
+        }
+
+        public void setSessionCookieName(String sessionCookieName) {
+            this.sessionCookieName = sessionCookieName;
+        }
+
+        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);
+            configure(handler);
+            return handler;
+        }
+
+        private void configure(SessionHandler handler) {
+            handler.setSessionTimeout(this.sessionTimeOut)
+                    .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<String> 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 00000000000..7ef4d3d8862
--- /dev/null
+++ b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSessionTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.Arrays;
+
+import io.restassured.RestAssured;
+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.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.junit.jupiter.api.Assertions.*;
+
+public class VertxPlatformHttpSessionTest {
+
+    @Test
+    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);
+        });
+
+        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)
+                    .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");
+
+            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)
+                    .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();
+        }
+    }
+
+    @Test
+    public void testSessionHandling() throws Exception {
+        int port = AvailablePortFinder.getNextAvailable();
+        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")
+                            .setBody().constant("session");
+
+                    from("direct:session")
+                            .toF("http://localhost:%d/session?cookieHandler=#instanceCookieHander",
+                                    port);
+                }
+            });
+
+            context.start();
+
+            // initial call establishes session
+            ProducerTemplate template = context.createProducerTemplate();
+            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");
+
+            // 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");
+
+        } finally {
+            context.stop();
+        }
+    }
+
+    private String getHeader(String header, Exchange exchange) {
+        return (String) exchange.getMessage().getHeader(header);
+    }
+
+    private CamelContext createCamelContext(SessionConfigCustomizer customizer)
+            throws Exception {
+        int bindPort = AvailablePortFinder.getNextAvailable();
+        RestAssured.port = bindPort;
+        return createCamelContext(bindPort, customizer);
+    }
+
+    private CamelContext createCamelContext(int bindPort, SessionConfigCustomizer customizer)
+            throws Exception {
+        VertxPlatformHttpServerConfiguration conf = new VertxPlatformHttpServerConfiguration();
+        conf.setBindPort(bindPort);
+
+        VertxPlatformHttpServerConfiguration.SessionConfig sessionConfig
+                = new VertxPlatformHttpServerConfiguration.SessionConfig();
+        customizer.customize(sessionConfig);
+        conf.setSessionConfig(sessionConfig);
+
+        CamelContext camelContext = new DefaultCamelContext();
+        camelContext.addService(new VertxPlatformHttpServer(conf));
+        return camelContext;
+    }
+
+    private void addPlatformHttpEngineHandler(CamelContext camelContext, Handler<RoutingContext> 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);
+    }
+
+    private class HitCountHandler implements Handler<RoutingContext> {
+        @Override
+        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();
+        }
+    }
+
+    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 2549c8f3911..4fea0c6c367 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
@@ -123,3 +123,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.