diff --git a/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java index 0ef247e2e7a..211e40acf98 100644 --- a/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java +++ b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java @@ -24,6 +24,8 @@ import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -146,6 +148,9 @@ public static KeyStore getDomain1TrustStore() { } + public static int getPort(HttpListenerType listenerType) { + return listenerType.getPort(); + } /** * Creates a {@link Client} instance for the domain administrator. * Caller is responsible for closing. @@ -204,6 +209,14 @@ public static T openConnection(final boolean secur return connection; } + public static URI webSocketUri(final int port, final String context) throws URISyntaxException { + return webSocketUri(false, port, context); + } + + public static URI webSocketUri(final boolean secured, final int port, final String context) throws URISyntaxException { + final String protocol = secured ? "wss" : "ws"; + return new URI(protocol + "://localhost:" + port + context); + } /** * Creates the unencrypted password file on the local file system and uses it to create the user diff --git a/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/HttpListenerType.java b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/HttpListenerType.java new file mode 100644 index 00000000000..6136258c679 --- /dev/null +++ b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/HttpListenerType.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.main.itest.tools; + +/** + * + * @author Ondro Mihalyi + */ +public enum HttpListenerType { + ADMIN("AS_ADMIN_PORT", 4848), HTTP("AS_HTTP_PORT", 8080), HTTPS("AS_HTTPS_PORT", 8181); + protected String variableName; + protected int defaultPort; + + private HttpListenerType(String variableName, int defaultPort) { + this.variableName = variableName; + this.defaultPort = defaultPort; + } + + public String getVariableName() { + return variableName; + } + + public int getDefaultPort() { + return defaultPort; + } + + public int getPort() { + return Integer.parseInt(System.getenv() + .getOrDefault(getVariableName(), String.valueOf(getDefaultPort())) + ); + } + +} diff --git a/appserver/tests/application/pom.xml b/appserver/tests/application/pom.xml index b0b77e70bc8..66d76f8ba35 100755 --- a/appserver/tests/application/pom.xml +++ b/appserver/tests/application/pom.xml @@ -16,7 +16,9 @@ SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 --> - + 4.0.0 @@ -106,6 +108,22 @@ jakarta.authentication-api provided + + jakarta.xml.ws + jakarta.xml.ws-api + provided + + + jakarta.websocket + jakarta.websocket-api + provided + + + jakarta.websocket + jakarta.websocket-client-api + provided + + jakarta.jws jakarta.jws-api @@ -115,12 +133,7 @@ jakarta.xml.soap jakarta.xml.soap-api - provided 3.0.2 - - - jakarta.xml.ws - jakarta.xml.ws-api provided @@ -140,6 +153,17 @@ ${project.version} provided + + + org.glassfish.tyrus + tyrus-client + test + + + org.glassfish.tyrus + tyrus-container-grizzly-client + test + diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/websocket/CustomRequestResponseServletFilter.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/websocket/CustomRequestResponseServletFilter.java new file mode 100644 index 00000000000..44d4df08484 --- /dev/null +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/websocket/CustomRequestResponseServletFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.main.test.app.websocket; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import java.io.IOException; +import java.util.logging.Logger; + +import static java.util.logging.Level.SEVERE; + +/** + * + * @author Ondro Mihalyi + */ +@WebFilter("/hello") +public class CustomRequestResponseServletFilter implements Filter { + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + ServletRequest wrappedRequest = new HttpServletRequestWrapper((HttpServletRequest) request) { + }; + ServletResponse wrappedResponse = new HttpServletResponseWrapper((HttpServletResponse) response) { + }; + try { + chain.doFilter(wrappedRequest, wrappedResponse); + } catch (Exception e) { + Logger.getLogger(this.getClass().getName()).log(SEVERE, e.getMessage(), e); + } + } +} diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/websocket/HelloWebSocketEndpoint.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/websocket/HelloWebSocketEndpoint.java new file mode 100644 index 00000000000..d0d4c1cb21a --- /dev/null +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/websocket/HelloWebSocketEndpoint.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.main.test.app.websocket; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.OnMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +import java.io.IOException; + +/** + * + * @author Ondro Mihalyi + */ +@ServerEndpoint("/hello") +@ApplicationScoped +public class HelloWebSocketEndpoint { + + @OnMessage + public void processGreeting(String message, Session session) throws IOException { + session.getBasicRemote().sendText("World"); + } +} diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/CustomRequestResponseOnWebSocketTest.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/CustomRequestResponseOnWebSocketTest.java new file mode 100644 index 00000000000..ffd32463a03 --- /dev/null +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/CustomRequestResponseOnWebSocketTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.main.test.app.websocket; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.glassfish.main.itest.tools.GlassFishTestEnvironment; +import org.glassfish.main.itest.tools.asadmin.Asadmin; +import org.glassfish.main.itest.tools.asadmin.AsadminResult; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static java.lang.System.Logger.Level.INFO; +import static org.glassfish.main.itest.tools.GlassFishTestEnvironment.webSocketUri; +import static org.glassfish.main.itest.tools.HttpListenerType.HTTP; +import static org.glassfish.main.itest.tools.asadmin.AsadminResultMatcher.asadminOK; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +/* + * Tests for https://github.com/eclipse-ee4j/glassfish/issues/24712 + */ +public class CustomRequestResponseOnWebSocketTest { + + private static final System.Logger LOG = System.getLogger(CustomRequestResponseOnWebSocketTest.class.getName()); + + private static final int HTTP_PORT = GlassFishTestEnvironment.getPort(HTTP); + + private static final String WEBAPP_FILE_NAME = "webapp.war"; + + private static final String WEBAPP_NAME = "webapp"; + + private static final Asadmin ASADMIN = GlassFishTestEnvironment.getAsadmin(); + + @TempDir + private static File webAppDir; + + @BeforeAll + public static void deployAll() throws IOException { + File webApp = createWebApp(); + + AsadminResult result = ASADMIN.exec("deploy", + "--contextroot", "/" + WEBAPP_NAME, + "--name", WEBAPP_NAME, + webApp.getAbsolutePath()); + assertThat(result, asadminOK()); + } + + @AfterAll + public static void undeployAll() { + assertAll( + () -> assertThat(ASADMIN.exec("undeploy", WEBAPP_NAME), asadminOK()) + ); + } + + @Test + public void testCustomRequest() throws IOException, URISyntaxException, InterruptedException, ExecutionException, TimeoutException { + final WebSocketClient webSocketClient = new WebSocketClient(webSocketUri(HTTP_PORT, "/" + WEBAPP_NAME + "/hello")); + webSocketClient.sendMessage("Hello"); + CompletableFuture waitForMessage = new CompletableFuture<>(); + webSocketClient.addMessageHandler(msg -> waitForMessage.complete(msg)); + + final String message = waitForMessage.get(10, TimeUnit.SECONDS); + + assertThat(message, equalTo("World")); + } + + private static File createWebApp() throws IOException { + WebArchive webArchive = ShrinkWrap.create(WebArchive.class) + .addClass(CustomRequestResponseServletFilter.class) + .addClass(HelloWebSocketEndpoint.class); + + LOG.log(INFO, webArchive.toString(true)); + + File webApp = new File(webAppDir, WEBAPP_FILE_NAME); + webArchive.as(ZipExporter.class).exportTo(webApp, true); + return webApp; + } +} diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/WebSocketClient.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/WebSocketClient.java new file mode 100644 index 00000000000..5c7ac15dba5 --- /dev/null +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/WebSocketClient.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.main.test.app.websocket; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.CloseReason; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import java.io.IOException; +import java.net.URI; +import java.util.function.Consumer; + +/** + * + * @author Ondro Mihalyi + */ +@ClientEndpoint +public class WebSocketClient { + + Session userSession = null; + private Consumer messageHandler; + + public WebSocketClient(URI endpointURI) { + try { + WebSocketContainer container = ContainerProvider + .getWebSocketContainer(); + container.connectToServer(this, endpointURI); + } catch (DeploymentException | IOException e) { + throw new RuntimeException(e); + } + } + + @OnOpen + public void onOpen(Session userSession) { + this.userSession = userSession; + } + + @OnClose + public void onClose(Session userSession, CloseReason reason) { + this.userSession = null; + } + + @OnMessage + public void onMessage(String message) { + if (this.messageHandler != null) + this.messageHandler.accept(message); + } + + public void addMessageHandler(Consumer msgHandler) { + this.messageHandler = msgHandler; + } + + public void sendMessage(String message) { + this.userSession.getAsyncRemote().sendText(message); + } + +} diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/WebSocketOnDefaultWebModuleTest.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/WebSocketOnDefaultWebModuleTest.java new file mode 100644 index 00000000000..663647fb787 --- /dev/null +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/websocket/WebSocketOnDefaultWebModuleTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.main.test.app.websocket; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.glassfish.main.itest.tools.GlassFishTestEnvironment; +import org.glassfish.main.itest.tools.asadmin.Asadmin; +import org.glassfish.main.itest.tools.asadmin.AsadminResult; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static java.lang.System.Logger.Level.INFO; +import static org.glassfish.main.itest.tools.GlassFishTestEnvironment.webSocketUri; +import static org.glassfish.main.itest.tools.HttpListenerType.HTTP; +import static org.glassfish.main.itest.tools.asadmin.AsadminResultMatcher.asadminOK; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +/* + * Tests for https://github.com/eclipse-ee4j/glassfish/pull/24691 + */ +public class WebSocketOnDefaultWebModuleTest { + + private static final System.Logger LOG = System.getLogger(WebSocketOnDefaultWebModuleTest.class.getName()); + + private static final int HTTP_PORT = GlassFishTestEnvironment.getPort(HTTP); + + private static final String WEBAPP_FILE_NAME = "webapp.war"; + + private static final String WEBAPP_NAME = "webapp"; + + private static final Asadmin ASADMIN = GlassFishTestEnvironment.getAsadmin(); + + @TempDir + private static File webAppDir; + + @BeforeAll + public static void deployAll() throws IOException { + File webApp = createWebApp(); + + AsadminResult result = ASADMIN.exec("deploy", + "--contextroot", "/" + WEBAPP_NAME, + "--name", WEBAPP_NAME, + webApp.getAbsolutePath()); + assertThat(result, asadminOK()); + + result = ASADMIN.exec("set", + "server-config.http-service.virtual-server.server.default-web-module=" + WEBAPP_NAME); + assertThat(result, asadminOK()); + } + + @AfterAll + public static void undeployAll() { + assertAll( + () -> assertThat(ASADMIN.exec("set", + "server-config.http-service.virtual-server.server.default-web-module="), asadminOK()), + () -> assertThat(ASADMIN.exec("undeploy", WEBAPP_NAME), asadminOK()) + ); + } + + @Test + public void testWebSocketOnDefaultWebModule() throws IOException, URISyntaxException, InterruptedException, ExecutionException, TimeoutException { + + final WebSocketClient webSocketClient = new WebSocketClient(webSocketUri(HTTP_PORT, "/hello")); + webSocketClient.sendMessage("Hello"); + CompletableFuture waitForMessage = new CompletableFuture<>(); + webSocketClient.addMessageHandler(msg -> waitForMessage.complete(msg)); + + final String message = waitForMessage.get(10, TimeUnit.SECONDS); + + assertThat(message, equalTo("World")); + } + + @Test + public void testWebSocketOnAppContextRoot() throws IOException, URISyntaxException, InterruptedException, ExecutionException, TimeoutException { + + final WebSocketClient webSocketClient = new WebSocketClient(webSocketUri(HTTP_PORT, "/" + WEBAPP_NAME + "/hello")); + webSocketClient.sendMessage("Hello"); + CompletableFuture waitForMessage = new CompletableFuture<>(); + webSocketClient.addMessageHandler(msg -> waitForMessage.complete(msg)); + + final String message = waitForMessage.get(10, TimeUnit.SECONDS); + + assertThat(message, equalTo("World")); + } + + private static File createWebApp() throws IOException { + WebArchive webArchive = ShrinkWrap.create(WebArchive.class) + .addClass(HelloWebSocketEndpoint.class); + + LOG.log(INFO, webArchive.toString(true)); + + File webApp = new File(webAppDir, WEBAPP_FILE_NAME); + webArchive.as(ZipExporter.class).exportTo(webApp, true); + return webApp; + } +} diff --git a/appserver/web/web-core/src/main/java/org/apache/catalina/core/WebSocketFilterWrapper.java b/appserver/web/web-core/src/main/java/org/apache/catalina/core/WebSocketFilterWrapper.java index 3281fd74bbc..6fa206b70ad 100644 --- a/appserver/web/web-core/src/main/java/org/apache/catalina/core/WebSocketFilterWrapper.java +++ b/appserver/web/web-core/src/main/java/org/apache/catalina/core/WebSocketFilterWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023, 2024 Contributors to the Eclipse Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestWrapper; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; @@ -71,7 +72,13 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha @Override public String getRequestURI() { - RequestFacade wrappedRequest = (RequestFacade) super.getRequest(); + RequestFacade wrappedRequest = findWrappedRequestFacade(); + + if (wrappedRequest == null) { + // fallback to the default behavior + return super.getRequestURI(); + } + String requestURI = wrappedRequest.getRequestURI(); // Get the contextPath without masking the default context mapping. @@ -83,6 +90,17 @@ public String getRequestURI() { return contextPath + requestURI; } + + private RequestFacade findWrappedRequestFacade() { + ServletRequest wrappedRequest = this.getRequest(); + while (wrappedRequest != null + && !(wrappedRequest instanceof RequestFacade) + && wrappedRequest instanceof ServletRequestWrapper) { + ServletRequestWrapper wrappedRequestWrapper = (ServletRequestWrapper)wrappedRequest; + wrappedRequest = wrappedRequestWrapper.getRequest(); + } + return (wrappedRequest instanceof RequestFacade) ? (RequestFacade)wrappedRequest : null; + } }; }