This is an automated email from the ASF dual-hosted git repository. bonampak pushed a commit to branch feature/jakarta-jetty-upgrade in repository https://gitbox.apache.org/repos/asf/knox.git
commit 0d14f0606b9513036bd68a22b39eb5ca140e234a Author: bonampak <[email protected]> AuthorDate: Mon Apr 27 18:17:43 2026 +0200 KNOX-3238: fix WebsocketServerInitiatedPingTest and WebsocketEchoHTTPServiceRoleTest.testGatewayRewriteHttpEcho --- .../gateway/websockets/KnoxWebSocketCreator.java | 18 ++++++-- .../websockets/WebsocketBackendUrlTest.java | 48 ++++++++++++++++++++++ .../WebsocketServerInitiatedPingTest.java | 44 +++++++++++++++----- 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java index d7e65f658..a73db079c 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java @@ -242,8 +242,13 @@ public class KnoxWebSocketCreator implements WebSocketCreator { final String protocol = (serviceUrl.getProtocol().equals("https")) ? "wss" : "ws"; backend.append(protocol).append("://"); backend.append(serviceUrl.getHost()).append(':'); - backend.append(serviceUrl.getPort()).append('/'); - backend.append(serviceUrl.getPath()); + backend.append(serviceUrl.getPort()); + // Only append a slash if getPath() doesn't already start with one + String serviceUrlPath = serviceUrl.getPath(); + if (StringUtils.isNotEmpty(serviceUrlPath) && !serviceUrlPath.startsWith("/")) { + backend.append('/'); + } + backend.append(serviceUrlPath); String pathSuffix = generateUrlSuffix(backend.toString(), pathService); backend.append(pathSuffix); } else { @@ -256,8 +261,13 @@ public class KnoxWebSocketCreator implements WebSocketCreator { : "ws"; backend.append(protocol).append("://"); backend.append(serviceUrl.getHost()).append(':'); - backend.append(serviceUrl.getPort()).append('/'); - backend.append(serviceUrl.getPath()); + backend.append(serviceUrl.getPort()); + // Only append a slash if getPath() doesn't already start with one + String serviceUrlPath = serviceUrl.getPath(); + if (StringUtils.isNotEmpty(serviceUrlPath) && !serviceUrlPath.startsWith("/")) { + backend.append('/'); + } + backend.append(serviceUrlPath); } /* in case we have query params */ if(!StringUtils.isBlank(query)) { diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java index b90948269..ae871e2e2 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java @@ -17,6 +17,13 @@ */ package org.apache.knox.gateway.websockets; +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.registry.ServiceDefEntry; +import org.apache.knox.gateway.services.registry.ServiceDefinitionRegistry; +import org.apache.knox.gateway.services.registry.ServiceRegistry; +import org.easymock.EasyMock; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -88,4 +95,45 @@ public class WebsocketBackendUrlTest extends WebsocketEchoTestBase { String expectedBackendUrl = backendServerUri.toString() + pathContext; assertThat(backendUrl, is(expectedBackendUrl)); } + + @Test + public void testMatchedBackendURLDoubleSlashPrevention() throws Exception { + GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); + ServiceRegistry serviceRegistry = EasyMock.createNiceMock(ServiceRegistry.class); + ServiceDefinitionRegistry serviceDefinitionRegistry = EasyMock.createNiceMock(ServiceDefinitionRegistry.class); + ServiceDefEntry serviceDefEntry = EasyMock.createNiceMock(ServiceDefEntry.class); + + EasyMock.expect(services.getService(ServiceType.SERVICE_REGISTRY_SERVICE)) + .andReturn(serviceRegistry).anyTimes(); + EasyMock.expect(services.getService(ServiceType.SERVICE_DEFINITION_REGISTRY)) + .andReturn(serviceDefinitionRegistry).anyTimes(); + + EasyMock.expect(serviceDefinitionRegistry.getMatchingService(EasyMock.anyString())) + .andReturn(serviceDefEntry).anyTimes(); + EasyMock.expect(serviceDefEntry.getName()) + .andReturn("WEBSOCKET").anyTimes(); + + // Simulate the backend topology returning an HTTP URL with a path that starts with a slash + EasyMock.expect(serviceRegistry.lookupServiceURL(EasyMock.anyString(), EasyMock.anyString())) + .andReturn("http://localhost:53170/ws").anyTimes(); + + EasyMock.replay(config, services, serviceRegistry, serviceDefinitionRegistry, serviceDefEntry); + + KnoxWebSocketCreator creator = new KnoxWebSocketCreator(config, services); + URI requestURI = new URI("ws://localhost:8443/gateway/websocket/123foo456bar/channels"); + String backendURL = creator.getMatchedBackendURL(requestURI); + + // Assert that the double slash is prevented + assertThat(backendURL, is("ws://localhost:53170/ws")); + + // Let's also verify that HTTPS correctly maps to WSS without double slashes + EasyMock.reset(serviceRegistry); + EasyMock.expect(serviceRegistry.lookupServiceURL(EasyMock.anyString(), EasyMock.anyString())) + .andReturn("https://localhost:53170/ws").anyTimes(); + EasyMock.replay(serviceRegistry); + + backendURL = creator.getMatchedBackendURL(requestURI); + assertThat(backendURL, is("wss://localhost:53170/ws")); + } } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java index da7078497..740a19067 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java @@ -18,7 +18,9 @@ package org.apache.knox.gateway.websockets; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.server.ServerUpgradeRequest; import org.eclipse.jetty.websocket.server.ServerUpgradeResponse; import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; @@ -32,6 +34,7 @@ import jakarta.websocket.WebSocketContainer; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import static org.hamcrest.CoreMatchers.is; @@ -59,13 +62,15 @@ import static org.hamcrest.MatcherAssert.assertThat; */ public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase { + private static WebsocketServerInitiatedPingHandler pingHandler; public WebsocketServerInitiatedPingTest() { super(); } @BeforeClass public static void setUpBeforeClass() throws Exception { - handler = new WebsocketServerInitiatedPingHandler(); + pingHandler = new WebsocketServerInitiatedPingHandler(); + handler = pingHandler; WebsocketEchoTestBase.setUpBeforeClass(); WebsocketEchoTestBase.startServers("ws"); } @@ -83,13 +88,15 @@ public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase { WebSocketContainer container = ContainerProvider.getWebSocketContainer(); WebsocketClient client = new WebsocketClient(); - container.connectToServer(client, - new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels")); - - //session.getBasicRemote().sendText("Echo"); - client.messageQueue.awaitMessages(1, 10000, TimeUnit.MILLISECONDS); - - assertThat(client.messageQueue.get(0), is("PingPong")); + try (jakarta.websocket.Session session = container.connectToServer(client, + new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels"))) { + assertThat(session.isOpen(), is(true)); + //session.getBasicRemote().sendText("Echo"); + // Wait for the backend server to receive the automatic PONG from Knox's JSR-356 container + String pongPayload = pingHandler.socket.pongFuture.get(10000, TimeUnit.MILLISECONDS); + + assertThat(pongPayload, is("PingPong")); + } } /** @@ -97,7 +104,7 @@ public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase { * */ private static class WebsocketServerInitiatedPingHandler extends AbstractWebSocketHandler { - + public final ServerInitiatingPingSocket socket = new ServerInitiatingPingSocket(); @Override protected void configure(ServerWebSocketContainer container) { container.setMaxTextMessageSize(2 * 1024 * 1024); @@ -105,7 +112,7 @@ public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase { @Override public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) { - return new ServerInitiatingPingSocket(); + return socket; } } @@ -113,6 +120,7 @@ public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase { * A simple socket initiating message on connect */ public static class ServerInitiatingPingSocket extends Session.Listener.AbstractAutoDemanding { + public final CompletableFuture<String> pongFuture = new CompletableFuture<>(); @Override public void onWebSocketError(Throwable cause) { @@ -138,5 +146,21 @@ public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase { // BatchMode and manual flushing are handled automatically by the Jetty engine. session.sendPing(binaryMessage, org.eclipse.jetty.websocket.api.Callback.NOOP); } + + @Override + public void onWebSocketFrame(Frame frame, org.eclipse.jetty.websocket.api.Callback callback) { + // Intercept PONG frames returning from Knox + if (frame.getOpCode() == OpCode.PONG) { + ByteBuffer payload = frame.getPayload(); + if (payload != null) { + byte[] bytes = new byte[payload.remaining()]; + payload.get(bytes); + pongFuture.complete(new String(bytes, StandardCharsets.UTF_8)); + } else { + pongFuture.complete(""); + } + } + super.onWebSocketFrame(frame, callback); + } } }
