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()));
