diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java index 347238eb702a..c2bdeed9721e 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java @@ -24,7 +24,6 @@ import javax.websocket.Extension.Parameter; import javax.websocket.server.ServerEndpointConfig; -import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.websocket.core.ExtensionConfig; @@ -139,14 +138,24 @@ public Map getUserProperties() resp.setExtensions(configs); // [JSR] Step 4: build out new ServerEndpointConfig - PathSpec pathSpec = jsrHandshakeRequest.getRequestPathSpec(); - if (pathSpec instanceof UriTemplatePathSpec) + Object pathSpecObject = jsrHandshakeRequest.getRequestPathSpec(); + if (pathSpecObject instanceof UriTemplatePathSpec) { - // We have a PathParam path spec - UriTemplatePathSpec wspathSpec = (UriTemplatePathSpec)pathSpec; - String requestPath = req.getRequestPath(); - // Wrap the config with the path spec information - config = new PathParamServerEndpointConfig(config, wspathSpec, requestPath); + // We can get path params from PathSpec and Request Path. + UriTemplatePathSpec pathSpec = (UriTemplatePathSpec)pathSpecObject; + Map pathParams = pathSpec.getPathParams(req.getRequestPath()); + + // Wrap the config with the path spec information. + config = new PathParamServerEndpointConfig(config, pathParams); + } + else + { + Map pathParams = jsrHandshakeRequest.getPathParams(); + if (pathParams != null) + { + // Wrap the config with the path spec information. + config = new PathParamServerEndpointConfig(config, pathParams); + } } // [JSR] Step 5: Call modifyHandshake diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java index 6683b931244d..051410dd5591 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java @@ -13,12 +13,16 @@ package org.eclipse.jetty.websocket.javax.server.internal; +import java.io.IOException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Function; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.websocket.DeploymentException; import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; @@ -34,7 +38,9 @@ import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException; import org.eclipse.jetty.websocket.core.internal.util.ReflectUtils; +import org.eclipse.jetty.websocket.core.server.Handshaker; import org.eclipse.jetty.websocket.core.server.WebSocketMappings; +import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; import org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator; @@ -46,6 +52,7 @@ public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer implements javax.websocket.server.ServerContainer, LifeCycle.Listener { public static final String JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE = javax.websocket.server.ServerContainer.class.getName(); + public static final String PATH_PARAM_ATTRIBUTE = "javax.websocket.server.pathParams"; private static final Logger LOG = LoggerFactory.getLogger(JavaxWebSocketServerContainer.class); public static JavaxWebSocketServerContainer getContainer(ServletContext servletContext) @@ -254,6 +261,35 @@ private void addEndpointMapping(ServerEndpointConfig config) throws DeploymentEx } } + public void upgradeHttpToWebSocket(Object httpServletRequest, Object httpServletResponse, ServerEndpointConfig sec, + Map pathParameters) throws IOException, DeploymentException + { + HttpServletRequest request = (HttpServletRequest)httpServletRequest; + HttpServletResponse response = (HttpServletResponse)httpServletResponse; + + // Decorate the provided Configurator. + components.getObjectFactory().decorate(sec.getConfigurator()); + + // If we have annotations merge the annotated ServerEndpointConfig with the provided one. + Class endpointClass = sec.getEndpointClass(); + ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class); + ServerEndpointConfig config = (anno == null) ? sec + : new AnnotatedServerEndpointConfig(this, endpointClass, anno, sec); + + if (LOG.isDebugEnabled()) + LOG.debug("addEndpoint({}) path={} endpoint={}", config, config.getPath(), endpointClass); + + validateEndpointConfig(config); + frameHandlerFactory.getMetadata(config.getEndpointClass(), config); + request.setAttribute(JavaxWebSocketServerContainer.PATH_PARAM_ATTRIBUTE, pathParameters); + + // Perform the upgrade. + JavaxWebSocketCreator creator = new JavaxWebSocketCreator(this, config, getExtensionRegistry()); + WebSocketNegotiator negotiator = WebSocketNegotiator.from(creator, frameHandlerFactory); + Handshaker handshaker = webSocketMappings.getHandshaker(); + handshaker.upgradeRequest(negotiator, request, response, components, defaultCustomizer); + } + @Override protected void doStart() throws Exception { diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java index 0d25082638a5..47f2ff76eed4 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java @@ -60,6 +60,12 @@ public PathSpec getRequestPathSpec() return (PathSpec)delegate.getServletAttribute(PathSpec.class.getName()); } + @SuppressWarnings("unchecked") + public Map getPathParams() + { + return (Map)delegate.getServletAttribute(JavaxWebSocketServerContainer.PATH_PARAM_ATTRIBUTE); + } + @Override public URI getRequestURI() { diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamServerEndpointConfig.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamServerEndpointConfig.java index 4d30fcf124b3..a95b60810cbf 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamServerEndpointConfig.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamServerEndpointConfig.java @@ -17,7 +17,6 @@ import java.util.Map; import javax.websocket.server.ServerEndpointConfig; -import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.websocket.javax.common.PathParamProvider; import org.eclipse.jetty.websocket.javax.common.ServerEndpointConfigWrapper; @@ -30,18 +29,13 @@ public class PathParamServerEndpointConfig extends ServerEndpointConfigWrapper i { private final Map pathParamMap; - public PathParamServerEndpointConfig(ServerEndpointConfig config, UriTemplatePathSpec pathSpec, String requestPath) + public PathParamServerEndpointConfig(ServerEndpointConfig config, Map pathParams) { super(config); - Map pathMap = pathSpec.getPathParams(requestPath); pathParamMap = new HashMap<>(); - if (pathMap != null) - { - pathMap.entrySet().stream().forEach( - entry -> pathParamMap.put(entry.getKey(), URIUtil.decodePath(entry.getValue())) - ); - } + if (pathParams != null) + pathParams.forEach((key, value) -> pathParamMap.put(key, URIUtil.decodePath(value))); } @Override diff --git a/jetty-websocket/websocket-javax-tests/pom.xml b/jetty-websocket/websocket-javax-tests/pom.xml index ef727aa8b1db..181b8b4914a8 100644 --- a/jetty-websocket/websocket-javax-tests/pom.xml +++ b/jetty-websocket/websocket-javax-tests/pom.xml @@ -35,6 +35,11 @@ org.eclipse.jetty.toolchain jetty-javax-websocket-api + + org.eclipse.jetty + jetty-util-ajax + test + org.eclipse.jetty jetty-slf4j-impl diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/ProgrammaticWebSocketUpgradeTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/ProgrammaticWebSocketUpgradeTest.java new file mode 100644 index 000000000000..a1fc86052e34 --- /dev/null +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/ProgrammaticWebSocketUpgradeTest.java @@ -0,0 +1,170 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.websocket.CloseReason; +import javax.websocket.DeploymentException; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpointConfig; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.ajax.JSON; +import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ProgrammaticWebSocketUpgradeTest +{ + private static final Map PATH_PARAMS = Map.of("param1", "value1", "param2", "value2"); + private static final JSON JSON = new JSON(); + private Server server; + private ServerConnector connector; + private JavaxWebSocketClientContainer client; + + @BeforeEach + public void before() throws Exception + { + client = new JavaxWebSocketClientContainer(); + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + contextHandler.addServlet(new ServletHolder(new CustomUpgradeServlet()), "/"); + server.setHandler(contextHandler); + + JavaxWebSocketServletContainerInitializer.configure(contextHandler, null); + + server.start(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + public static class PathParamsEndpoint extends Endpoint + { + @Override + public void onOpen(Session session, EndpointConfig config) + { + try + { + session.getBasicRemote().sendText(JSON.toJSON(session.getPathParameters())); + session.close(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + } + + public static class CustomUpgradeServlet extends HttpServlet + { + private JavaxWebSocketServerContainer container; + + @Override + public void init(ServletConfig config) throws ServletException + { + super.init(config); + container = JavaxWebSocketServerContainer.getContainer(getServletContext()); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + switch (request.getServletPath()) + { + case "/echo": + { + ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(EchoSocket.class, "/").build(); + HashMap pathParams = new HashMap<>(); + container.upgradeHttpToWebSocket(request, response, sec, pathParams); + break; + } + case "/pathParams": + { + ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(PathParamsEndpoint.class, "/").build(); + container.upgradeHttpToWebSocket(request, response, sec, PATH_PARAMS); + break; + } + default: + throw new IllegalStateException(); + } + } + catch (DeploymentException e) + { + throw new ServletException(e); + } + } + } + + @Test + public void testWebSocketUpgrade() throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/echo"); + EventSocket socket = new EventSocket(); + try (Session session = client.connectToServer(socket, uri)) + { + session.getBasicRemote().sendText("hello world"); + } + assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS)); + + String msg = socket.textMessages.poll(); + assertThat(msg, is("hello world")); + assertThat(socket.closeReason.getCloseCode(), is(CloseReason.CloseCodes.NORMAL_CLOSURE)); + } + + @Test + public void testPathParameters() throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/pathParams"); + EventSocket socket = new EventSocket(); + client.connectToServer(socket, uri); + assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS)); + + String msg = socket.textMessages.poll(); + assertThat(JSON.fromJSON(msg), is(PATH_PARAMS)); + assertThat(socket.closeReason.getCloseCode(), is(CloseReason.CloseCodes.NORMAL_CLOSURE)); + } +}