This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 13a78504f2c5 CAMEL-25375: camel-undertow - Run WebSocket handlers
checks for consumers only, and start the servlet context before the server
(#27443)
13a78504f2c5 is described below
commit 13a78504f2c5254efb774828c471e0159d6fcf54
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]>
---
.../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()));