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

davsclaus pushed a commit to branch backport/CAMEL-25375-camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 0807599736988618262a102f172a03d6ec584f67
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Oct 6 15:17:20 2026 +0200

    CAMEL-25375: camel-undertow - Run WebSocket handlers checks for consumers 
only, and start the servlet context before the server (#27443)
    
    - handlers is a consumer option: it only counts as a security check of a
      WebSocket path when the consumer endpoint sets it, so a producer that
      sets it no longer makes its path close every connection.
    - Endpoints that configure the same security configuration no longer log
      that the security settings of the producer are not used.
    - The servlet context of the first endpoint of a server is started before
      the server, so that a failure leaves no server running, and a failure to
      start the server undeploys it.
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
    (cherry picked from commit 13a78504f2c5254efb774828c471e0159d6fcf54)
---
 .../component/undertow/DefaultUndertowHost.java    |  9 +++
 .../undertow/handlers/CamelWebSocketHandler.java   | 69 ++++++++++++++--------
 .../CamelWebSocketHandlerSecuritySettingsTest.java | 24 ++++++++
 .../ws/UndertowWsSecurityWithoutProviderTest.java  | 20 +++++++
 4 files changed, 98 insertions(+), 24 deletions(-)

diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java
index 1e33a6dd8b3f..e50aeef51405 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java
@@ -136,6 +136,10 @@ public class DefaultUndertowHost implements UndertowHost {
                 // use the rest handler as its a rest consumer
                 serverHandler = consumer != null && consumer.isRest() ? 
restHandler : rootHandler;
                 entryHandler = serverHandler;
+                if (requiresServletContext(endpoint)) {
+                    // deploy before the server starts, so that a failure 
leaves no server running
+                    deployServletContext();
+                }
                 undertow = builder.setHandler(exchange -> 
entryHandler.handleRequest(exchange)).build();
                 LOG.info("Starting Undertow server on {}://{}:{}", 
key.getSslContext() != null ? "https" : "http",
                         key.getHost(),
@@ -156,11 +160,16 @@ public class DefaultUndertowHost implements UndertowHost {
                     // initialization again.
                     undertow.stop();
                     undertow = null;
+                    if (deploymentManager != null) {
+                        deploymentManager.undeploy();
+                        deploymentManager = null;
+                    }
 
                     throw e;
                 }
             }
             if (deploymentManager == null && requiresServletContext(endpoint)) 
{
+                // a later endpoint needs a servlet context: wrap the handler 
of the running server
                 deployServletContext();
             }
             if (consumer != null && consumer.isRest()) {
diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandler.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandler.java
index cf46381bde97..9d855f8e0742 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandler.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandler.java
@@ -205,8 +205,8 @@ public class CamelWebSocketHandler implements HttpHandler {
     }
 
     private void upgrade(HttpServerExchange exchange) throws Exception {
-        List<UndertowEndpoint> endpoints = securityEndpoints();
-        if 
(endpoints.stream().noneMatch(CamelWebSocketHandler::hasSecurityChecks)) {
+        PathSecurity path = pathSecurity();
+        if (path.endpoints.stream().noneMatch(endpoint -> 
hasSecurityChecks(endpoint, path.consumer))) {
             this.delegate.handleRequest(exchange);
             return;
         }
@@ -216,7 +216,7 @@ public class CamelWebSocketHandler implements HttpHandler {
         }
         Set<String> authenticatedEndpoints = new HashSet<>();
         Map<String, Object> headers = new HashMap<>();
-        for (UndertowEndpoint endpoint : endpoints) {
+        for (UndertowEndpoint endpoint : path.endpoints) {
             OAuthHttpSecuritySupport oauthHttpSecurity = 
endpoint.getOauthHttpSecurity();
             if (oauthHttpSecurity != null) {
                 Validation validation = 
oauthHttpSecurity.validate(endpoint.getCamelContext(), 
authorizationHeaders(exchange));
@@ -238,7 +238,7 @@ public class CamelWebSocketHandler implements HttpHandler {
                     endpoint.getSecurityProvider().addHeader(headers::put, 
exchange);
                 }
             }
-            if (requiresHandshakeResult(endpoint) && (endpoint.getHandlers() 
== null
+            if (requiresHandshakeResult(endpoint, path.consumer) && 
(!path.consumer || endpoint.getHandlers() == null
                     || 
endpoint.getEndpointUri().equals(exchange.getAttachment(CONSUMER_HANDLERS_ATTACHMENT))))
 {
                 authenticatedEndpoints.add(endpoint.getEndpointUri());
             }
@@ -254,33 +254,33 @@ public class CamelWebSocketHandler implements HttpHandler 
{
      * The endpoints whose security settings apply to the path: the 
consumer's, also while it is stopped, otherwise the
      * producers'. All the Camel endpoints of a path share its WebSocket 
connections.
      */
-    private List<UndertowEndpoint> securityEndpoints() {
+    private PathSecurity pathSecurity() {
         consumerLock.lock();
         try {
             if (consumerEndpoint != null) {
-                return List.of(consumerEndpoint);
+                return new PathSecurity(List.of(consumerEndpoint), true);
             }
         } finally {
             consumerLock.unlock();
         }
-        return producerEndpoints.stream().distinct().toList();
+        return new 
PathSecurity(producerEndpoints.stream().distinct().toList(), false);
     }
 
-    private static boolean hasSecurityChecks(UndertowEndpoint endpoint) {
-        return endpoint.getOauthHttpSecurity() != null || 
requiresHandshakeResult(endpoint);
+    private static boolean hasSecurityChecks(UndertowEndpoint endpoint, 
boolean consumer) {
+        return endpoint.getOauthHttpSecurity() != null || 
requiresHandshakeResult(endpoint, consumer);
     }
 
     /**
-     * Whether a channel must have passed the security provider, allowed roles 
or custom handlers of the endpoint during
-     * its handshake.
+     * Whether a channel must have passed the security provider, the allowed 
roles or, for the endpoint of a consumer,
+     * the custom handlers of the endpoint during its handshake. Only 
consumers run their custom handlers.
      */
-    private static boolean requiresHandshakeResult(UndertowEndpoint endpoint) {
-        return endpoint.requiresAuthentication() || endpoint.getHandlers() != 
null;
+    private static boolean requiresHandshakeResult(UndertowEndpoint endpoint, 
boolean consumer) {
+        return endpoint.requiresAuthentication() || consumer && 
endpoint.getHandlers() != null;
     }
 
-    private static boolean isAuthenticated(WebSocketChannel channel, 
List<UndertowEndpoint> endpoints) {
-        for (UndertowEndpoint endpoint : endpoints) {
-            if (!isAuthenticated(channel, endpoint)) {
+    private static boolean isAuthenticated(WebSocketChannel channel, 
PathSecurity path) {
+        for (UndertowEndpoint endpoint : path.endpoints) {
+            if (!isAuthenticated(channel, endpoint, path.consumer)) {
                 return false;
             }
         }
@@ -288,17 +288,21 @@ public class CamelWebSocketHandler implements HttpHandler 
{
     }
 
     /**
-     * Whether the handshake of the given channel passed the security checks 
of the given endpoint: its OAuth
-     * validation, its security provider, its allowed roles and its custom 
handlers. Always {@code true} for an endpoint
-     * without such checks.
+     * Whether the handshake of the given channel passed the security checks 
of the given endpoint of a consumer: its
+     * OAuth validation, its security provider, its allowed roles and its 
custom handlers. Always {@code true} for an
+     * endpoint without such checks.
      */
     public static boolean isAuthenticated(WebSocketChannel channel, 
UndertowEndpoint endpoint) {
+        return isAuthenticated(channel, endpoint, true);
+    }
+
+    private static boolean isAuthenticated(WebSocketChannel channel, 
UndertowEndpoint endpoint, boolean consumer) {
         if (endpoint.getOauthHttpSecurity() != null && (channel == null
                 || !(channel.getAttribute(
                         
OAuthHttpSecuritySupport.OAUTH_TOKEN_VALIDATION_RESULT) instanceof 
OAuthTokenValidationResult))) {
             return false;
         }
-        if (requiresHandshakeResult(endpoint)) {
+        if (requiresHandshakeResult(endpoint, consumer)) {
             return channel != null
                     && channel.getAttribute(HANDSHAKE_RESULT) instanceof 
HandshakeResult result
                     && result.endpointUris.contains(endpoint.getEndpointUri());
@@ -353,9 +357,9 @@ public class CamelWebSocketHandler implements HttpHandler {
             final Exchange camelExchange, final AsyncCallback camelCallback)
             throws IOException {
         // only the peers whose handshake passed the security checks of the 
path receive messages
-        List<UndertowEndpoint> endpoints = securityEndpoints();
+        PathSecurity path = pathSecurity();
         List<WebSocketChannel> targetPeers = 
delegate.getPeerConnections().stream()
-                .filter(peer -> isAuthenticated(peer, endpoints))
+                .filter(peer -> isAuthenticated(peer, path))
                 .filter(peerFilter)
                 .collect(Collectors.toList());
         if (targetPeers.isEmpty()) {
@@ -437,7 +441,11 @@ public class CamelWebSocketHandler implements HttpHandler {
         boolean ownSettings = producerEndpoint.getAllowedRoles() != null
                 || producerEndpoint.getSecurityConfiguration() != null
                 || producerEndpoint.getSecurityProvider() != 
producerEndpoint.getComponent().getSecurityProvider();
-        return ownSettings && (producerEndpoint.getSecurityProvider() != 
consumerEndpoint.getSecurityProvider()
+        // endpoints that configure the same security configuration each get a 
provider of their own, which are alike
+        boolean sameProvider = producerEndpoint.getSecurityProvider() == 
consumerEndpoint.getSecurityProvider()
+                || producerEndpoint.getSecurityConfiguration() != null
+                        && producerEndpoint.getSecurityConfiguration() == 
consumerEndpoint.getSecurityConfiguration();
+        return ownSettings && (!sameProvider
                 || !Objects.equals(producerEndpoint.computeAllowedRoles(), 
consumerEndpoint.computeAllowedRoles()));
     }
 
@@ -627,6 +635,19 @@ public class CamelWebSocketHandler implements HttpHandler {
 
     }
 
+    /**
+     * The endpoints whose security settings apply to a path, and whether that 
is the endpoint of its consumer.
+     */
+    private static final class PathSecurity {
+        private final List<UndertowEndpoint> endpoints;
+        private final boolean consumer;
+
+        private PathSecurity(List<UndertowEndpoint> endpoints, boolean 
consumer) {
+            this.endpoints = endpoints;
+            this.consumer = consumer;
+        }
+    }
+
     /**
      * The handlers of a consumer, which end with {@link #upgradeHandler}, and 
the endpoint of that consumer.
      */
@@ -674,7 +695,7 @@ public class CamelWebSocketHandler implements HttpHandler {
             if (handshakeResult != null) {
                 channel.setAttribute(HANDSHAKE_RESULT, handshakeResult);
             }
-            if (!isAuthenticated(channel, securityEndpoints())) {
+            if (!isAuthenticated(channel, pathSecurity())) {
                 // the handshake did not pass the security checks that now 
apply to the path, for example because it
                 // completed before a consumer requiring them was set on this 
handler: fail closed
                 LOG.warn("Closing WebSocket channel whose handshake did not 
pass the security checks");
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandlerSecuritySettingsTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandlerSecuritySettingsTest.java
index 6e26a728f74a..69c6187ea203 100644
--- 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandlerSecuritySettingsTest.java
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/handlers/CamelWebSocketHandlerSecuritySettingsTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.undertow.handlers;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.component.undertow.UndertowEndpoint;
+import org.apache.camel.component.undertow.spi.AbstractSecurityProviderTest;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.junit.jupiter.api.Test;
 
@@ -50,6 +51,29 @@ class CamelWebSocketHandlerSecuritySettingsTest {
         
assertTrue(hasUnusedSecuritySettings("fireWebSocketChannelEvents=true", 
"allowedRoles=admin"));
     }
 
+    @Test
+    void producerWithTheSameSecurityConfigurationAsTheConsumer() throws 
Exception {
+        // each endpoint that configures a security configuration gets a 
provider instance of its own
+        Object configuration = new Object();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.start();
+            UndertowEndpoint consumerEndpoint = endpoint(context, 
"allowedRoles=user", configuration);
+            UndertowEndpoint producerEndpoint = endpoint(context, 
"allowedRoles=user&sendToAll=true", configuration);
+            
assertFalse(CamelWebSocketHandler.hasUnusedSecuritySettings(consumerEndpoint, 
producerEndpoint));
+
+            UndertowEndpoint otherProducerEndpoint = endpoint(context, 
"allowedRoles=user&sendToAll=false", new Object());
+            
assertTrue(CamelWebSocketHandler.hasUnusedSecuritySettings(consumerEndpoint, 
otherProducerEndpoint));
+        }
+    }
+
+    private static UndertowEndpoint endpoint(CamelContext context, String 
options, Object securityConfiguration) {
+        UndertowEndpoint endpoint
+                = context.getEndpoint("undertow:ws://localhost:8080/path?" + 
options, UndertowEndpoint.class);
+        endpoint.setSecurityConfiguration(securityConfiguration);
+        endpoint.setSecurityProvider(new 
AbstractSecurityProviderTest.MockSecurityProvider());
+        return endpoint;
+    }
+
     private static boolean hasUnusedSecuritySettings(String consumerOptions, 
String producerOptions) throws Exception {
         try (CamelContext context = new DefaultCamelContext()) {
             context.start();
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/ws/UndertowWsSecurityWithoutProviderTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/ws/UndertowWsSecurityWithoutProviderTest.java
index 8da47b15535f..09f301add9ea 100644
--- 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/ws/UndertowWsSecurityWithoutProviderTest.java
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/ws/UndertowWsSecurityWithoutProviderTest.java
@@ -43,6 +43,7 @@ import org.junit.jupiter.api.Test;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -61,6 +62,7 @@ class UndertowWsSecurityWithoutProviderTest extends 
BaseUndertowTest {
         context.getRegistry().bind(OAuthTokenValidationFactory.FACTORY, new 
StubOAuthTokenValidationFactory());
         context.getRegistry().bind("basicAuth", new 
UndertowBasicAuthHandler());
         context.getRegistry().bind("lateBasicAuth", new 
UndertowBasicAuthHandler());
+        context.getRegistry().bind("producerBasicAuth", new 
UndertowBasicAuthHandler());
         return context;
     }
 
@@ -80,6 +82,10 @@ class UndertowWsSecurityWithoutProviderTest extends 
BaseUndertowTest {
                 
from("undertow:ws://localhost:{{port}}/late?allowedRoles=user").routeId("late").autoStartup(false)
                         .process(exchange -> 
lateRouteInvocations.incrementAndGet());
 
+                // handlers is a consumer option: a producer does not run it
+                from("direct:producerHandlers")
+                        
.to("undertow:ws://localhost:{{port}}/producerHandlers?handlers=#producerBasicAuth&sendToAll=true");
+
                 
from("direct:lateBasic").to("undertow:ws://localhost:{{port}}/lateBasic?sendToAll=true");
                 
from("undertow:ws://localhost:{{port}}/lateBasic?handlers=#lateBasicAuth").routeId("lateBasic")
                         .autoStartup(false)
@@ -155,6 +161,20 @@ class UndertowWsSecurityWithoutProviderTest extends 
BaseUndertowTest {
         authenticated.sendClose(WebSocket.NORMAL_CLOSURE, "done").join();
     }
 
+    @Test
+    void handlersOfAProducerDoNotGuardItsPath() {
+        RecordingListener listener = new RecordingListener();
+        WebSocket webSocket = connect("/producerHandlers", null, listener);
+
+        // the server registers the connection shortly after the client has 
completed the upgrade
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            template.sendBody("direct:producerHandlers", "update");
+            assertFalse(listener.received.isEmpty());
+        });
+        assertEquals("update", listener.received.get(0));
+        webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "done").join();
+    }
+
     private void assertRefused(String path, String authorization, int 
statusCode) {
         CompletionException thrown
                 = assertThrows(CompletionException.class, () -> connect(path, 
authorization, new RecordingListener()));

Reply via email to