diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/listener/ContainerInitializer.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/listener/ContainerInitializer.java new file mode 100644 index 00000000000..b1eb8fe9124 --- /dev/null +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/listener/ContainerInitializer.java @@ -0,0 +1,185 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.servlet.listener; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * Utility Methods for manual execution of {@link javax.servlet.ServletContainerInitializer} when + * using Embedded Jetty. + */ +public final class ContainerInitializer +{ + /** + * Utility Method to allow for manual execution of {@link javax.servlet.ServletContainerInitializer} when + * using Embedded Jetty. + * + * + * ServletContextHandler context = new ServletContextHandler(); + * ServletContainerInitializer corpSci = new MyCorporateSCI(); + * context.addEventListener(ContainerInitializer.asContextListener(corpSci)); + * + * + *

+ * The {@link ServletContainerInitializer} will have its {@link ServletContainerInitializer#onStartup(Set, ServletContext)} + * method called with the manually configured list of {@code Set> c} set. + * In other words, this usage does not perform bytecode or annotation scanning against the classes in + * your {@code ServletContextHandler} or {@code WebAppContext}. + *

+ * + * @param sci the {@link ServletContainerInitializer} to call + * @return the {@link ServletContextListener} wrapping the SCI + * @see SCIAsContextListener#addClasses(Class[]) + * @see SCIAsContextListener#addClasses(String...) + */ + public static SCIAsContextListener asContextListener(ServletContainerInitializer sci) + { + return new SCIAsContextListener(sci); + } + + public static class SCIAsContextListener implements ServletContextListener + { + private final ServletContainerInitializer sci; + private Set classNames; + private Set> classes = new HashSet<>(); + private Consumer postOnStartupConsumer; + + public SCIAsContextListener(ServletContainerInitializer sci) + { + this.sci = sci; + } + + /** + * Add classes to be passed to the {@link ServletContainerInitializer#onStartup(Set, ServletContext)} call. + *

+ * Note that these classes will be loaded using the context classloader for the ServletContext + * initialization phase. + *

+ * + * @param classNames the class names to load and pass into the {@link ServletContainerInitializer#onStartup(Set, ServletContext)} call + * @return this configured {@link SCIAsContextListener} instance. + */ + public SCIAsContextListener addClasses(String... classNames) + { + if (this.classNames == null) + { + this.classNames = new HashSet<>(); + } + this.classNames.addAll(Arrays.asList(classNames)); + return this; + } + + /** + * Add classes to be passed to the {@link ServletContainerInitializer#onStartup(Set, ServletContext)} call. + *

+ * Note that these classes will exist on the classloader that was used to call this method. + * If you want the classes to be loaded using the context classloader for the ServletContext + * then use the String form of the classes via the {@link #addClasses(String...)} method. + *

+ * + * @param classes the classes to pass into the {@link ServletContainerInitializer#onStartup(Set, ServletContext)} call + * @return this configured {@link SCIAsContextListener} instance. + */ + public SCIAsContextListener addClasses(Class... classes) + { + this.classes.addAll(Arrays.asList(classes)); + return this; + } + + /** + * Add a optional consumer to execute once the {@link ServletContainerInitializer#onStartup(Set, ServletContext)} has + * been called successfully. + *

+ * This would be for actions to perform on a ServletContext once this specific SCI has completed + * its execution. Actions that would require specific configurations that the SCI provides to be present on the + * ServletContext to function properly. + *

+ *

+ * This consumer is typically used for Embedded Jetty users to configure Jetty for their specific needs. + *

+ * + * + * @param consumer the consumer to execute after the SCI has executed + * @return this configured {@link SCIAsContextListener} instance. + */ + public SCIAsContextListener setPostOnStartupConsumer(Consumer consumer) + { + this.postOnStartupConsumer = consumer; + return this; + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + ServletContext servletContext = sce.getServletContext(); + try + { + sci.onStartup(getClasses(), servletContext); + if (postOnStartupConsumer != null) + { + postOnStartupConsumer.accept(servletContext); + } + } + catch (RuntimeException rte) + { + throw rte; + } + catch (Throwable cause) + { + throw new RuntimeException(cause); + } + } + + public Set> getClasses() + { + if (classNames != null && !classNames.isEmpty()) + { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + for (String className : classNames) + { + try + { + Class clazz = cl.loadClass(className); + classes.add(clazz); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Unable to find class: " + className, e); + } + } + } + + return classes; + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + // ignore + } + } +} diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java index fabb5b82cbc..02bcc59b607 100644 --- a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java +++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.websocket.jsr356.server.deploy; import java.util.HashSet; import java.util.Set; - import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; @@ -34,8 +33,10 @@ import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.listener.ContainerInitializer; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -53,7 +54,8 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit public static final String ADD_DYNAMIC_FILTER_KEY = "org.eclipse.jetty.websocket.jsr356.addDynamicFilter"; private static final Logger LOG = Log.getLogger(WebSocketServerContainerInitializer.class); public static final String HTTPCLIENT_ATTRIBUTE = "org.eclipse.jetty.websocket.jsr356.HttpClient"; - + public static final String ATTR_JAVAX_SERVER_CONTAINER = javax.websocket.server.ServerContainer.class.getName(); + /** * DestroyListener */ @@ -129,61 +131,145 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit return defValue; } - + + public interface Configurator + { + void accept(ServletContext servletContext, ServerContainer serverContainer) throws DeploymentException; + } + /** - * Embedded Jetty approach for non-bytecode scanning. * @param context the {@link ServletContextHandler} to use * @return a configured {@link ServerContainer} instance * @throws ServletException if the {@link WebSocketUpgradeFilter} cannot be configured + * @deprecated use {@link #configure(ServletContextHandler, Configurator)} instead + * @see #configure(ServletContextHandler, Configurator) */ + @Deprecated public static ServerContainer configureContext(ServletContextHandler context) throws ServletException { - // Create Basic components - NativeWebSocketConfiguration nativeWebSocketConfiguration = NativeWebSocketServletContainerInitializer.getDefaultFrom(context.getServletContext()); - - // Build HttpClient - HttpClient httpClient = (HttpClient) context.getServletContext().getAttribute(HTTPCLIENT_ATTRIBUTE); - if ((httpClient == null) && (context.getServer() != null)) - { - httpClient = (HttpClient) context.getServer().getAttribute(HTTPCLIENT_ATTRIBUTE); - } - - // Create the Jetty ServerContainer implementation - ServerContainer jettyContainer = new ServerContainer(nativeWebSocketConfiguration, httpClient); - context.addBean(jettyContainer); - - // Store a reference to the ServerContainer per javax.websocket spec 1.0 final section 6.4 Programmatic Server Deployment - context.setAttribute(javax.websocket.server.ServerContainer.class.getName(),jettyContainer); - - // Create Filter - if(isEnabledViaContext(context.getServletContext(), ADD_DYNAMIC_FILTER_KEY, true)) - { - String instanceKey = WebSocketUpgradeFilter.class.getName() + ".SCI"; - if(context.getAttribute(instanceKey) == null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dynamic filter add to support JSR356/javax.websocket.server: {}", WebSocketUpgradeFilter.class.getName()); - WebSocketUpgradeFilter wsuf = WebSocketUpgradeFilter.configureContext(context); - context.setAttribute(instanceKey, wsuf); - } - } - - return jettyContainer; + ServletContext servletContext = context.getServletContext(); + initialize(servletContext); + return (ServerContainer)servletContext.getAttribute(ATTR_JAVAX_SERVER_CONTAINER); } - + /** - * @deprecated use {@link #configureContext(ServletContextHandler)} instead * @param context not used * @param jettyContext the {@link ServletContextHandler} to use * @return a configured {@link ServerContainer} instance * @throws ServletException if the {@link WebSocketUpgradeFilter} cannot be configured + * @deprecated use {@link #configure(ServletContextHandler, Configurator)} instead + * @see #configure(ServletContextHandler, Configurator) */ @Deprecated public static ServerContainer configureContext(ServletContext context, ServletContextHandler jettyContext) throws ServletException { - return configureContext(jettyContext); + initialize(context); + return (ServerContainer)context.getAttribute(ATTR_JAVAX_SERVER_CONTAINER); } + /** + * Initialize the {@link ServletContext} with the default (and empty) {@link ServerContainer}. + * + *

+ * This performs a subset of the behaviors that {@link #onStartup(Set, ServletContext)} does. + * There is no enablement check here, and no automatic deployment of endpoints at this point + * in time. It merely sets up the {@link ServletContext} so with the basics needed to start + * configuring for `javax.websocket.server` based endpoints. + *

+ * + * @param context the context to work with + */ + public static void initialize(ServletContext context) throws ServletException + { + // Create Basic components + NativeWebSocketServletContainerInitializer.initialize(context); + NativeWebSocketConfiguration nativeWebSocketConfiguration = (NativeWebSocketConfiguration)context.getAttribute(NativeWebSocketServletContainerInitializer.ATTR_KEY); + + ContextHandler contextHandler = null; + // Attach default configuration to context lifecycle + if (context instanceof ContextHandler.Context) + { + contextHandler = ((ContextHandler.Context)context).getContextHandler(); + } + + // Obtain HttpClient + HttpClient httpClient = (HttpClient)context.getAttribute(HTTPCLIENT_ATTRIBUTE); + if ((httpClient == null) && (contextHandler != null)) + { + Server server = contextHandler.getServer(); + if (server != null) + { + httpClient = (HttpClient)server.getAttribute(HTTPCLIENT_ATTRIBUTE); + } + } + + // Create the Jetty ServerContainer implementation + ServerContainer jettyContainer = new ServerContainer(nativeWebSocketConfiguration, httpClient); + contextHandler.addBean(jettyContainer); + + // Store a reference to the ServerContainer per javax.websocket spec 1.0 final section 6.4 Programmatic Server Deployment + context.setAttribute(javax.websocket.server.ServerContainer.class.getName(),jettyContainer); + + if(contextHandler instanceof ServletContextHandler) + { + ServletContextHandler servletContextHandler = (ServletContextHandler)contextHandler; + // Create Filter + if(isEnabledViaContext(context, ADD_DYNAMIC_FILTER_KEY, true)) + { + String instanceKey = WebSocketUpgradeFilter.class.getName() + ".SCI"; + if(context.getAttribute(instanceKey) == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("Dynamic filter add to support JSR356/javax.websocket.server: {}", WebSocketUpgradeFilter.class.getName()); + WebSocketUpgradeFilter wsuf = WebSocketUpgradeFilter.configureContext(servletContextHandler); + context.setAttribute(instanceKey, wsuf); + } + } + } + } + + /** + * Configure the {@link ServletContextHandler} to call {@link WebSocketServerContainerInitializer#onStartup(Set, ServletContext)} + * during the {@link ServletContext} initialization phase. + * + * @param context the context to add listener to + */ + public static void configure(ServletContextHandler context) + { + context.addEventListener(ContainerInitializer.asContextListener(new WebSocketServerContainerInitializer())); + } + + /** + * Configure the {@link ServletContextHandler} to call {@link WebSocketServerContainerInitializer#onStartup(Set, ServletContext)} + * during the {@link ServletContext} initialization phase. + * + * @param context the context to add listener to + * @param configurator the lambda that is called to allow the {@link ServerContainer} to + * be configured during the {@link ServletContext} initialization phase + */ + public static void configure(ServletContextHandler context, Configurator configurator) + { + // In this embedded-jetty usage, allow ServletContext.addListener() to + // add other ServletContextListeners (such as the ContextDestroyListener) after + // the initialization phase is over. (important for this SCI to function) + context.getServletContext().setExtendedListenerTypes(true); + + context.addEventListener( + ContainerInitializer.asContextListener(new WebSocketServerContainerInitializer()) + .setPostOnStartupConsumer((servletContext) -> + { + ServerContainer serverContainer = (ServerContainer)servletContext.getAttribute(ATTR_JAVAX_SERVER_CONTAINER); + try + { + configurator.accept(servletContext, serverContainer); + } + catch (DeploymentException e) + { + throw new RuntimeException("Failed to deploy WebSocket Endpoint", e); + } + })); + } + @Override public void onStartup(Set> c, ServletContext context) throws ServletException { @@ -205,12 +291,11 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit throw new ServletException("Not running in Jetty ServletContextHandler, JSR-356 support unavailable"); } - ServletContextHandler jettyContext = (ServletContextHandler)handler; - try(ThreadClassLoaderScope scope = new ThreadClassLoaderScope(context.getClassLoader())) { // Create the Jetty ServerContainer implementation - ServerContainer jettyContainer = configureContext(jettyContext); + initialize(context); + ServerContainer jettyContainer = (ServerContainer)context.getAttribute(ATTR_JAVAX_SERVER_CONTAINER); context.addListener(new ContextDestroyListener()); // make sure context is cleaned up when the context stops diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java index 0018d9ec0eb..bd2075f90e7 100644 --- a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.websocket.jsr356.server; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -29,7 +26,6 @@ import java.net.URI; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.websocket.ClientEndpoint; import javax.websocket.ContainerProvider; import javax.websocket.OnMessage; @@ -43,10 +39,12 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class BinaryStreamTest { private static final String PATH = "/echo"; @@ -63,9 +61,11 @@ public class BinaryStreamTest server.addConnector(connector); ServletContextHandler context = new ServletContextHandler(server, "/", true, false); - ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); - ServerEndpointConfig config = ServerEndpointConfig.Builder.create(ServerBinaryStreamer.class, PATH).build(); - container.addEndpoint(config); + WebSocketServerContainerInitializer.configure(context, (servletContext, container) -> + { + ServerEndpointConfig config = ServerEndpointConfig.Builder.create(ServerBinaryStreamer.class, PATH).build(); + container.addEndpoint(config); + }); server.start(); diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java index ea45a3d2fda..21c8e52115c 100644 --- a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java @@ -19,14 +19,10 @@ package org.eclipse.jetty.websocket.jsr356.server.browser; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.Objects; -import javax.servlet.ServletException; -import javax.websocket.DeploymentException; - import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; @@ -37,7 +33,6 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.websocket.jsr356.server.ServerContainer; import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; /** @@ -68,7 +63,6 @@ public class JsrBrowserDebugTool JsrBrowserDebugTool tool = new JsrBrowserDebugTool(); tool.setupServer(port); tool.server.start(); - tool.server.dumpStdErr(); LOG.info("Server available at {}", tool.server.getURI()); tool.server.join(); } @@ -80,12 +74,10 @@ public class JsrBrowserDebugTool private Server server; - private ServerContainer setupServer(int port) throws DeploymentException, ServletException, URISyntaxException, MalformedURLException, IOException + private void setupServer(int port) throws URISyntaxException, IOException { server = new Server(); - server.setDumpAfterStart(true); - HttpConfiguration httpConf = new HttpConfiguration(); httpConf.setSendServerVersion(true); @@ -106,10 +98,9 @@ public class JsrBrowserDebugTool holder.setInitParameter("dirAllowed","true"); server.setHandler(context); - ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); - container.addEndpoint(JsrBrowserSocket.class); + WebSocketServerContainerInitializer.configure(context, + (servletContext, container) -> container.addEndpoint(JsrBrowserSocket.class)); LOG.info("{} setup on port {}",this.getClass().getName(),port); - return container; } } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/AnnoMaxMessageEndpoint.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/AnnoMaxMessageEndpoint.java new file mode 100644 index 00000000000..ad6d06b37d3 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/AnnoMaxMessageEndpoint.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +@SuppressWarnings("unused") +@WebSocket(maxTextMessageSize = 100*1024) +public class AnnoMaxMessageEndpoint +{ + @OnWebSocketMessage + public void onMessage(Session session, String msg) throws IOException + { + session.getRemote().sendString(msg); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/ConnectMessageEndpoint.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/ConnectMessageEndpoint.java new file mode 100644 index 00000000000..d9ff09a4645 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/ConnectMessageEndpoint.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +@SuppressWarnings("unused") +@WebSocket +public class ConnectMessageEndpoint +{ + @OnWebSocketConnect + public void onConnect(Session session) throws IOException + { + session.getRemote().sendString("Greeting from onConnect"); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java index 4908c6cb29c..ce2d9f651af 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; +@SuppressWarnings("unused") @WebSocket public class EchoSocket { diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/GetAuthHeaderEndpoint.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/GetAuthHeaderEndpoint.java new file mode 100644 index 00000000000..8e6130841cf --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/GetAuthHeaderEndpoint.java @@ -0,0 +1,38 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +@SuppressWarnings("unused") +@WebSocket +public class GetAuthHeaderEndpoint +{ + @OnWebSocketConnect + public void onConnect(Session session) throws IOException + { + String authHeaderName = "Authorization"; + String authHeaderValue = session.getUpgradeRequest().getHeader(authHeaderName); + session.getRemote().sendString("Header[" + authHeaderName + "]=" + authHeaderValue); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/InvalidUpgradeServlet.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/InvalidUpgradeServlet.java new file mode 100644 index 00000000000..44b49eeef3f --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/InvalidUpgradeServlet.java @@ -0,0 +1,68 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.websocket.common.AcceptHash; + +public class InvalidUpgradeServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + String pathInfo = req.getPathInfo(); + if (pathInfo.contains("only-accept")) + { + // Force 200 response, no response body content, incomplete websocket response headers, no actual upgrade for this test + resp.setStatus(HttpServletResponse.SC_OK); + String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); + } + else if (pathInfo.contains("close-connection")) + { + // Force 101 response, with invalid Connection header, invalid handshake + resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); + resp.setHeader(HttpHeader.CONNECTION.toString(), "close"); + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); + } + else if (pathInfo.contains("missing-connection")) + { + // Force 101 response, with no Connection header, invalid handshake + resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); + // Intentionally leave out Connection header + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); + } + else if (pathInfo.contains("rubbish-accept")) + { + // Force 101 response, with no Connection header, invalid handshake + resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), "rubbish"); + } + else + { + resp.setStatus(500); + } + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/ParamsEndpoint.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/ParamsEndpoint.java new file mode 100644 index 00000000000..bd4470465f1 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/ParamsEndpoint.java @@ -0,0 +1,49 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +@SuppressWarnings("unused") +@WebSocket +public class ParamsEndpoint +{ + @OnWebSocketConnect + public void onConnect(Session session) throws IOException + { + Map> params = session.getUpgradeRequest().getParameterMap(); + StringBuilder msg = new StringBuilder(); + + for (String key : params.keySet()) + { + msg.append("Params[").append(key).append("]="); + msg.append(params.get(key).stream().collect(Collectors.joining(", ", "[", "]"))); + msg.append("\n"); + } + + session.getRemote().sendString(msg.toString()); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SimpleStatusServlet.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SimpleStatusServlet.java new file mode 100644 index 00000000000..300439a10d4 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SimpleStatusServlet.java @@ -0,0 +1,41 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class SimpleStatusServlet extends HttpServlet +{ + private final int statusCode; + + public SimpleStatusServlet(int statusCode) + { + this.statusCode = statusCode; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(this.statusCode); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java new file mode 100644 index 00000000000..343ec19e095 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java @@ -0,0 +1,436 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests.client; + +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.EnumSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.servlet.DispatcherType; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.MappedByteBufferPool; +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.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeException; +import org.eclipse.jetty.websocket.api.util.WSURI; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.client.WebSocketUpgradeRequest; +import org.eclipse.jetty.websocket.server.NativeWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; +import org.eclipse.jetty.websocket.tests.EchoSocket; +import org.eclipse.jetty.websocket.tests.GetAuthHeaderEndpoint; +import org.eclipse.jetty.websocket.tests.InvalidUpgradeServlet; +import org.eclipse.jetty.websocket.tests.SimpleStatusServlet; +import org.hamcrest.Matcher; +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.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Various connect condition testing + */ +@SuppressWarnings("Duplicates") +public class ClientConnectTest +{ + public ByteBufferPool bufferPool = new MappedByteBufferPool(); + + private Server server; + private WebSocketClient client; + + @SuppressWarnings("unchecked") + private E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher errorMatcher) + { + // Validate thrown cause + Throwable cause = e.getCause(); + + assertThat("ExecutionException.cause", cause, errorMatcher); + + // Validate websocket captured cause + Throwable capcause = wsocket.error.get(); + assertThat("Error", capcause, notNullValue()); + assertThat("Error", capcause, errorMatcher); + + // Validate that websocket didn't see an open event + assertThat("Open Latch", wsocket.openLatch.getCount(), is(1L)); + + // Return the captured cause + return (E)capcause; + } + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.setBufferPool(bufferPool); + client.setConnectTimeout(TimeUnit.SECONDS.toMillis(3)); + client.setMaxIdleTimeout(TimeUnit.SECONDS.toMillis(3)); + client.getPolicy().setIdleTimeout(TimeUnit.SECONDS.toMillis(10)); + client.start(); + } + + @BeforeEach + public void startServer() throws Exception + { + server = new Server(); + + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + + NativeWebSocketServletContainerInitializer.configure(context, + (servletContext, configuration) -> + { + configuration.getPolicy().setIdleTimeout(10000); + configuration.addMapping("/echo", (req, resp) -> + { + if (req.hasSubProtocol("echo")) + resp.setAcceptedSubProtocol("echo"); + return new EchoSocket(); + }); + configuration.addMapping("/get-auth-header", (req, resp) -> new GetAuthHeaderEndpoint()); + }); + + context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + + context.addServlet(new ServletHolder(new SimpleStatusServlet(404)), "/bogus"); + context.addServlet(new ServletHolder(new SimpleStatusServlet(200)), "/a-okay"); + context.addServlet(new ServletHolder(new InvalidUpgradeServlet()), "/invalid-upgrade/*"); + + server.setHandler(context); + + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterEach + public void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testUpgradeRequest() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/echo")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock, wsUri); + + try (Session sess = future.get(30, TimeUnit.SECONDS)) + { + assertThat("Connect.UpgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Connect.UpgradeResponse", sess.getUpgradeResponse(), notNullValue()); + } + } + + @Test + public void testAltConnect() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/echo")); + HttpClient httpClient = new HttpClient(); + try + { + httpClient.start(); + + WebSocketUpgradeRequest req = new WebSocketUpgradeRequest(new WebSocketClient(), httpClient, wsUri, cliSock); + req.header("X-Foo", "Req"); + CompletableFuture sess = req.sendAsync(); + + sess.thenAccept((s) -> + { + System.out.printf("Session: %s%n", s); + s.close(); + assertThat("Connect.UpgradeRequest", s.getUpgradeRequest(), notNullValue()); + assertThat("Connect.UpgradeResponse", s.getUpgradeResponse(), notNullValue()); + }); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testUpgradeWithAuthorizationHeader() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/get-auth-header")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + // actual value for this test is irrelevant, its important that this + // header actually be sent with a value (the value specified) + String authHeaderValue = "Basic YWxhZGRpbjpvcGVuc2VzYW1l"; + request.setHeader("Authorization", authHeaderValue); + Future future = client.connect(cliSock, wsUri, request); + + try (Session sess = future.get(5, TimeUnit.SECONDS)) + { + // Test client side + String cliAuthValue = sess.getUpgradeRequest().getHeader("Authorization"); + assertThat("Client Request Authorization Value", cliAuthValue, is(authHeaderValue)); + + // wait for response from server + String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); + assertThat("Message", received, containsString("Header[Authorization]=" + authHeaderValue)); + } + } + + @Test + public void testBadHandshake() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/bogus")); + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI", ue.getRequestURI(), notNullValue()); + assertThat("UpgradeException.requestURI", ue.getRequestURI().toASCIIString(), is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode", ue.getResponseStatusCode(), is(404)); + } + + @Test + public void testBadHandshake_GetOK() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/a-okay")); + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI", ue.getRequestURI(), notNullValue()); + assertThat("UpgradeException.requestURI", ue.getRequestURI().toASCIIString(), is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode", ue.getResponseStatusCode(), is(200)); + } + + @Test + public void testBadHandshake_GetOK_WithSecWebSocketAccept() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/invalid-upgrade/only-accept")); + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI", ue.getRequestURI(), notNullValue()); + assertThat("UpgradeException.requestURI", ue.getRequestURI().toASCIIString(), is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode", ue.getResponseStatusCode(), is(200)); + } + + @Test + public void testBadHandshake_SwitchingProtocols_InvalidConnectionHeader() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/invalid-upgrade/close-connection")); + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI", ue.getRequestURI(), notNullValue()); + assertThat("UpgradeException.requestURI", ue.getRequestURI().toASCIIString(), is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode", ue.getResponseStatusCode(), is(101)); + } + + @Test + public void testBadHandshake_SwitchingProtocols_NoConnectionHeader() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/invalid-upgrade/missing-connection")); + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI", ue.getRequestURI(), notNullValue()); + assertThat("UpgradeException.requestURI", ue.getRequestURI().toASCIIString(), is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode", ue.getResponseStatusCode(), is(101)); + } + + @Test + public void testBadUpgrade() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/invalid-upgrade/rubbish-accept")); + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + assertThat("UpgradeException.responseStatusCode", ue.getResponseStatusCode(), is(101)); + } + + @Test + public void testConnectionNotAccepted() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + try (ServerSocket serverSocket = new ServerSocket()) + { + InetAddress addr = InetAddress.getByName("localhost"); + InetSocketAddress endpoint = new InetSocketAddress(addr, 0); + serverSocket.bind(endpoint, 1); + int port = serverSocket.getLocalPort(); + + URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); + Future future = client.connect(cliSock, wsUri); + + // Intentionally not accept incoming socket. + // serverSocket.accept(); + + try + { + future.get(8, TimeUnit.SECONDS); + fail("Should have Timed Out"); + } + catch (ExecutionException e) + { + // Passing Path (active session wait timeout) + assertExpectedError(e, cliSock, instanceOf(UpgradeException.class)); + } + catch (TimeoutException e) + { + // Passing Path + } + } + } + + @Test + public void testConnectionRefused() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + // Intentionally bad port with nothing listening on it + URI wsUri = new URI("ws://127.0.0.1:1"); + + try + { + Future future = client.connect(cliSock, wsUri); + + // The attempt to get upgrade response future should throw error + future.get(5, TimeUnit.SECONDS); + fail("Expected ExecutionException -> ConnectException"); + } + catch (ConnectException e) + { + Throwable t = cliSock.error.get(); + assertThat("Error Queue[0]", t, instanceOf(ConnectException.class)); + } + catch (ExecutionException e) + { + assertExpectedError(e, cliSock, + anyOf( + instanceOf(UpgradeException.class), + instanceOf(SocketTimeoutException.class), + instanceOf(ConnectException.class))); + } + } + + @Test + public void testConnectionTimeout_Concurrent() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + try (ServerSocket serverSocket = new ServerSocket()) + { + InetAddress addr = InetAddress.getByName("localhost"); + InetSocketAddress endpoint = new InetSocketAddress(addr, 0); + serverSocket.bind(endpoint, 1); + int port = serverSocket.getLocalPort(); + URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); + Future future = client.connect(cliSock, wsUri); + + // Accept the connection, but do nothing on it (no response, no upgrade, etc) + serverSocket.accept(); + + // The attempt to get upgrade response future should throw error + Exception e = assertThrows(Exception.class, + () -> future.get(5, TimeUnit.SECONDS)); + + if (e instanceof ExecutionException) + { + assertExpectedError((ExecutionException)e, cliSock, anyOf( + instanceOf(ConnectException.class), + instanceOf(UpgradeException.class) + )); + } + else + { + assertThat("Should have been a TimeoutException", e, instanceOf(TimeoutException.class)); + } + } + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java new file mode 100644 index 00000000000..fd8ecddf230 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java @@ -0,0 +1,333 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.servlet.DispatcherType; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.api.util.WSURI; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.common.io.FutureWriteCallback; +import org.eclipse.jetty.websocket.server.NativeWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.tests.AnnoMaxMessageEndpoint; +import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; +import org.eclipse.jetty.websocket.tests.ConnectMessageEndpoint; +import org.eclipse.jetty.websocket.tests.EchoSocket; +import org.eclipse.jetty.websocket.tests.ParamsEndpoint; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class WebSocketClientTest +{ + private Server server; + private WebSocketClient client; + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.start(); + } + + @BeforeEach + public void startServer() throws Exception + { + server = new Server(); + + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + + NativeWebSocketServletContainerInitializer.configure(context, + (servletContext, configuration) -> + { + configuration.getPolicy().setIdleTimeout(10000); + configuration.addMapping("/echo", (req, resp) -> + { + if (req.hasSubProtocol("echo")) + resp.setAcceptedSubProtocol("echo"); + return new EchoSocket(); + }); + configuration.addMapping("/anno-max-message", (req, resp) -> new AnnoMaxMessageEndpoint()); + configuration.addMapping("/connect-msg", (req, resp) -> new ConnectMessageEndpoint()); + configuration.addMapping("/get-params", (req, resp) -> new ParamsEndpoint()); + }); + + context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + + server.setHandler(context); + + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterEach + public void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testAddExtension_NotInstalled() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/echo")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + request.addExtensions("x-bad"); + + assertThrows(IllegalArgumentException.class, () -> + { + // Should trigger failure on bad extension + client.connect(cliSock, wsUri, request); + }); + } + + @Test + public void testBasicEcho_FromClient() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/echo")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock, wsUri, request); + + try (Session sess = future.get(30, TimeUnit.SECONDS)) + { + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); + + Collection sessions = client.getOpenSessions(); + assertThat("client.sessions.size", sessions.size(), is(1)); + + RemoteEndpoint remote = cliSock.getSession().getRemote(); + remote.sendString("Hello World!"); + + // wait for response from server + String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); + assertThat("Message", received, containsString("Hello World")); + } + } + + @Test + public void testBasicEcho_UsingCallback() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/echo")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock, wsUri, request); + + try (Session sess = future.get(5, TimeUnit.SECONDS)) + { + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); + + Collection sessions = client.getOpenSessions(); + assertThat("client.sessions.size", sessions.size(), is(1)); + + FutureWriteCallback callback = new FutureWriteCallback(); + + cliSock.getSession().getRemote().sendString("Hello World!", callback); + callback.get(5, TimeUnit.SECONDS); + + // wait for response from server + String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); + assertThat("Message", received, containsString("Hello World")); + } + } + + @Test + public void testBasicEcho_FromServer() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/connect-msg")); + Future future = client.connect(cliSock, wsUri); + + try (Session sess = future.get(5, TimeUnit.SECONDS)) + { + // Validate connect + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); + + // wait for message from server + String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); + assertThat("Message", received, containsString("Greeting from onConnect")); + } + } + + @Test + public void testLocalRemoteAddress() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/echo")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock, wsUri, request); + + try (Session sess = future.get(5, TimeUnit.SECONDS)) + { + Assertions.assertTrue(cliSock.openLatch.await(1, TimeUnit.SECONDS)); + + InetSocketAddress local = cliSock.getSession().getLocalAddress(); + InetSocketAddress remote = cliSock.getSession().getRemoteAddress(); + + assertThat("Local Socket Address", local, notNullValue()); + assertThat("Remote Socket Address", remote, notNullValue()); + + // Hard to validate (in a portable unit test) the local address that was used/bound in the low level Jetty Endpoint + assertThat("Local Socket Address / Host", local.getAddress().getHostAddress(), notNullValue()); + assertThat("Local Socket Address / Port", local.getPort(), greaterThan(0)); + + String uriHostAddress = InetAddress.getByName(wsUri.getHost()).getHostAddress(); + assertThat("Remote Socket Address / Host", remote.getAddress().getHostAddress(), is(uriHostAddress)); + assertThat("Remote Socket Address / Port", remote.getPort(), greaterThan(0)); + } + } + + /** + * Ensure that @WebSocket(maxTextMessageSize = 100*1024) behaves as expected. + * + * @throws Exception on test failure + */ + @Test + public void testMaxMessageSize() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setMaxTextMessageSize(100 * 1024); + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/anno-max-message")); + Future future = client.connect(cliSock, wsUri); + + try (Session sess = future.get(5, TimeUnit.SECONDS)) + { + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + + // Create string that is larger than default size of 64k + // but smaller than maxMessageSize of 100k + int size = 80 * 1024; + byte buf[] = new byte[size]; + Arrays.fill(buf,(byte)'x'); + String msg = StringUtil.toUTF8String(buf,0,buf.length); + + sess.getRemote().sendString(msg); + + // wait for message from server + String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); + assertThat("Message", received.length(), is(size)); + } + } + + @Test + public void testParameterMap() throws Exception + { + CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); + + client.getPolicy().setMaxTextMessageSize(100 * 1024); + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/get-params?snack=cashews&amount=handful&brand=off")); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + Future future = client.connect(cliSock, wsUri, request); + + try (Session sess = future.get(5, TimeUnit.SECONDS)) + { + UpgradeRequest req = sess.getUpgradeRequest(); + assertThat("Upgrade Request", req, notNullValue()); + + Map> parameterMap = req.getParameterMap(); + assertThat("Parameter Map", parameterMap, notNullValue()); + + assertThat("Parameter[snack]", parameterMap.get("snack"), is(Arrays.asList(new String[]{"cashews"}))); + assertThat("Parameter[amount]", parameterMap.get("amount"), is(Arrays.asList(new String[]{"handful"}))); + assertThat("Parameter[brand]", parameterMap.get("brand"), is(Arrays.asList(new String[]{"off"}))); + + assertThat("Parameter[cost]", parameterMap.get("cost"), nullValue()); + + // wait for message from server indicating what it sees + String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); + assertThat("Parameter[snack]", received, containsString("Params[snack]=[cashews]")); + assertThat("Parameter[amount]", received, containsString("Params[amount]=[handful]")); + assertThat("Parameter[brand]", received, containsString("Params[brand]=[off]")); + assertThat("Parameter[cost]", received, not(containsString("Params[cost]="))); + } + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java deleted file mode 100644 index 811718de27d..00000000000 --- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java +++ /dev/null @@ -1,463 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.client; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; - -import java.net.ConnectException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.SocketTimeoutException; -import java.net.URI; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.MappedByteBufferPool; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.UpgradeException; -import org.eclipse.jetty.websocket.common.AcceptHash; -import org.eclipse.jetty.websocket.common.test.BlockheadConnection; -import org.eclipse.jetty.websocket.common.test.BlockheadServer; -import org.eclipse.jetty.websocket.common.test.Timeouts; -import org.hamcrest.Matcher; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Various connect condition testing - */ -@SuppressWarnings("Duplicates") -public class ClientConnectTest -{ - public ByteBufferPool bufferPool = new MappedByteBufferPool(); - - private static BlockheadServer server; - private WebSocketClient client; - - @SuppressWarnings("unchecked") - private E assertExpectedError(ExecutionException e, JettyTrackingSocket wsocket, Matcher errorMatcher) - { - // Validate thrown cause - Throwable cause = e.getCause(); - - assertThat("ExecutionException.cause",cause,errorMatcher); - - // Validate websocket captured cause - assertThat("Error Queue Length",wsocket.errorQueue.size(),greaterThanOrEqualTo(1)); - Throwable capcause = wsocket.errorQueue.poll(); - assertThat("Error Queue[0]",capcause,notNullValue()); - assertThat("Error Queue[0]",capcause,errorMatcher); - - // Validate that websocket didn't see an open event - wsocket.assertNotOpened(); - - // Return the captured cause - return (E)capcause; - } - - @BeforeEach - public void startClient() throws Exception - { - client = new WebSocketClient(); - client.setBufferPool(bufferPool); - client.setConnectTimeout(Timeouts.CONNECT_UNIT.toMillis(Timeouts.CONNECT)); - client.start(); - } - - @BeforeAll - public static void startServer() throws Exception - { - server = new BlockheadServer(); - server.start(); - } - - @BeforeEach - public void resetServerHandler() - { - // for each test, reset the server request handling to default - server.resetRequestHandling(); - } - - @AfterEach - public void stopClient() throws Exception - { - client.stop(); - } - - @AfterAll - public static void stopServer() throws Exception - { - server.stop(); - } - - @Test - public void testUpgradeRequest() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - Session sess = future.get(30,TimeUnit.SECONDS); - - wsocket.waitForConnected(); - - assertThat("Connect.UpgradeRequest", wsocket.connectUpgradeRequest, notNullValue()); - assertThat("Connect.UpgradeResponse", wsocket.connectUpgradeResponse, notNullValue()); - - sess.close(); - } - - @Test - public void testAltConnect() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - URI wsUri = server.getWsUri(); - - HttpClient httpClient = new HttpClient(); - try - { - httpClient.start(); - - WebSocketUpgradeRequest req = new WebSocketUpgradeRequest(new WebSocketClient(), httpClient, wsUri, wsocket); - req.header("X-Foo", "Req"); - CompletableFuture sess = req.sendAsync(); - - sess.thenAccept((s) -> { - System.out.printf("Session: %s%n", s); - s.close(); - assertThat("Connect.UpgradeRequest", wsocket.connectUpgradeRequest, notNullValue()); - assertThat("Connect.UpgradeResponse", wsocket.connectUpgradeResponse, notNullValue()); - }); - } - finally - { - httpClient.stop(); - } - } - - @Test - public void testUpgradeWithAuthorizationHeader() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Hook into server connection creation - CompletableFuture serverConnFut = new CompletableFuture<>(); - server.addConnectFuture(serverConnFut); - - URI wsUri = server.getWsUri(); - ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); - // actual value for this test is irrelevant, its important that this - // header actually be sent with a value (the value specified) - upgradeRequest.setHeader("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); - Future future = client.connect(wsocket,wsUri,upgradeRequest); - - try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) - { - HttpFields upgradeRequestHeaders = serverConn.getUpgradeRequestHeaders(); - - Session sess = future.get(30, TimeUnit.SECONDS); - - HttpField authHeader = upgradeRequestHeaders.getField(HttpHeader.AUTHORIZATION); - assertThat("Server Request Authorization Header", authHeader, is(notNullValue())); - assertThat("Server Request Authorization Value", authHeader.getValue(), is("Basic YWxhZGRpbjpvcGVuc2VzYW1l")); - assertThat("Connect.UpgradeRequest", wsocket.connectUpgradeRequest, notNullValue()); - assertThat("Connect.UpgradeResponse", wsocket.connectUpgradeResponse, notNullValue()); - - sess.close(); - } - } - - @Test - public void testBadHandshake() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Force 404 response, no upgrade for this test - server.setRequestHandling((req, resp) -> { - resp.setStatus(HttpServletResponse.SC_NOT_FOUND); - return true; - }); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - ExecutionException e = assertThrows(ExecutionException.class, - ()-> future.get(30,TimeUnit.SECONDS)); - - UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); - assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); - assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); - assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(404)); - } - - @Test - public void testBadHandshake_GetOK() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Force 200 response, no response body content, no upgrade for this test - server.setRequestHandling((req, resp) -> { - resp.setStatus(HttpServletResponse.SC_OK); - return true; - }); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - ExecutionException e = assertThrows(ExecutionException.class, - ()-> future.get(30,TimeUnit.SECONDS)); - - UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); - assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); - assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); - assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(200)); - } - - @Test - public void testBadHandshake_GetOK_WithSecWebSocketAccept() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Force 200 response, no response body content, incomplete websocket response headers, no actual upgrade for this test - server.setRequestHandling((req, resp) -> { - String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); - resp.setStatus(HttpServletResponse.SC_OK); - resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); - return true; - }); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - ExecutionException e = assertThrows(ExecutionException.class, - ()-> future.get(30,TimeUnit.SECONDS)); - - UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); - assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); - assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); - assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(200)); - } - - @Test - public void testBadHandshake_SwitchingProtocols_InvalidConnectionHeader() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Force 101 response, with invalid Connection header, invalid handshake - server.setRequestHandling((req, resp) -> { - String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); - resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); - resp.setHeader(HttpHeader.CONNECTION.toString(), "close"); - resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); - return true; - }); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - ExecutionException e = assertThrows(ExecutionException.class, - ()-> future.get(30,TimeUnit.SECONDS)); - - UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); - assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); - assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); - assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(101)); - } - - @Test - public void testBadHandshake_SwitchingProtocols_NoConnectionHeader() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Force 101 response, with no Connection header, invalid handshake - server.setRequestHandling((req, resp) -> { - String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); - resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); - // Intentionally leave out Connection header - resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); - return true; - }); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - ExecutionException e = assertThrows(ExecutionException.class, - ()-> future.get(30,TimeUnit.SECONDS)); - - UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); - assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); - assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); - assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(101)); - } - - @Test - public void testBadUpgrade() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Force 101 response, with invalid response accept header - server.setRequestHandling((req, resp) -> { - resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); - resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), "rubbish"); - return true; - }); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - ExecutionException e = assertThrows(ExecutionException.class, - ()-> future.get(30,TimeUnit.SECONDS)); - - UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); - assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); - assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); - assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(101)); - } - - @Test - public void testConnectionNotAccepted() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - try(ServerSocket serverSocket = new ServerSocket()) - { - InetAddress addr = InetAddress.getByName("localhost"); - InetSocketAddress endpoint = new InetSocketAddress(addr, 0); - serverSocket.bind(endpoint, 1); - int port = serverSocket.getLocalPort(); - URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); - Future future = client.connect(wsocket, wsUri); - - // Intentionally not accept incoming socket. - // serverSocket.accept(); - - try - { - future.get(3, TimeUnit.SECONDS); - fail("Should have Timed Out"); - } - catch (ExecutionException e) - { - assertExpectedError(e, wsocket, instanceOf(UpgradeException.class)); - // Possible Passing Path (active session wait timeout) - wsocket.assertNotOpened(); - } - catch (TimeoutException e) - { - // Possible Passing Path (concurrency timeout) - wsocket.assertNotOpened(); - } - } - } - - @Test - public void testConnectionRefused() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - // Intentionally bad port with nothing listening on it - URI wsUri = new URI("ws://127.0.0.1:1"); - - try - { - Future future = client.connect(wsocket,wsUri); - - // The attempt to get upgrade response future should throw error - future.get(3,TimeUnit.SECONDS); - fail("Expected ExecutionException -> ConnectException"); - } - catch (ConnectException e) - { - Throwable t = wsocket.errorQueue.remove(); - assertThat("Error Queue[0]",t,instanceOf(ConnectException.class)); - wsocket.assertNotOpened(); - } - catch (ExecutionException e) - { - assertExpectedError(e, wsocket, - anyOf( - instanceOf(UpgradeException.class), - instanceOf(SocketTimeoutException.class), - instanceOf(ConnectException.class))); - } - } - - @Test - public void testConnectionTimeout_Concurrent() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - try(ServerSocket serverSocket = new ServerSocket()) - { - InetAddress addr = InetAddress.getByName("localhost"); - InetSocketAddress endpoint = new InetSocketAddress(addr, 0); - serverSocket.bind(endpoint, 1); - int port = serverSocket.getLocalPort(); - URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); - Future future = client.connect(wsocket, wsUri); - - // Accept the connection, but do nothing on it (no response, no upgrade, etc) - serverSocket.accept(); - - // The attempt to get upgrade response future should throw error - Exception e = assertThrows(Exception.class, - ()-> future.get(3, TimeUnit.SECONDS)); - - if (e instanceof ExecutionException) - { - assertExpectedError((ExecutionException) e, wsocket, anyOf( - instanceOf(ConnectException.class), - instanceOf(UpgradeException.class) - )); - } - else - { - assertThat("Should have been a TimeoutException", e, instanceOf(TimeoutException.class)); - } - } - } -} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java deleted file mode 100644 index adee6b543d4..00000000000 --- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java +++ /dev/null @@ -1,328 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.client; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.websocket.api.BatchMode; -import org.eclipse.jetty.websocket.api.RemoteEndpoint; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.common.WebSocketFrame; -import org.eclipse.jetty.websocket.common.WebSocketSession; -import org.eclipse.jetty.websocket.common.frames.TextFrame; -import org.eclipse.jetty.websocket.common.io.FutureWriteCallback; -import org.eclipse.jetty.websocket.common.test.BlockheadConnection; -import org.eclipse.jetty.websocket.common.test.BlockheadServer; -import org.eclipse.jetty.websocket.common.test.Timeouts; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class WebSocketClientTest -{ - private static BlockheadServer server; - private WebSocketClient client; - - @BeforeEach - public void startClient() throws Exception - { - client = new WebSocketClient(); - client.start(); - } - - @BeforeAll - public static void startServer() throws Exception - { - server = new BlockheadServer(); - server.getPolicy().setMaxTextMessageSize(200 * 1024); - server.getPolicy().setMaxBinaryMessageSize(200 * 1024); - server.start(); - } - - @AfterEach - public void stopClient() throws Exception - { - client.stop(); - } - - @AfterAll - public static void stopServer() throws Exception - { - server.stop(); - } - - @Test - public void testAddExtension_NotInstalled() throws Exception - { - JettyTrackingSocket cliSock = new JettyTrackingSocket(); - - client.getPolicy().setIdleTimeout(10000); - - URI wsUri = server.getWsUri(); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - request.setSubProtocols("echo"); - request.addExtensions("x-bad"); - - assertThrows(IllegalArgumentException.class, ()-> { - // Should trigger failure on bad extension - client.connect(cliSock, wsUri, request); - }); - } - - @Test - public void testBasicEcho_FromClient() throws Exception - { - JettyTrackingSocket cliSock = new JettyTrackingSocket(); - - client.getPolicy().setIdleTimeout(10000); - - // Hook into server connection creation - CompletableFuture serverConnFut = new CompletableFuture<>(); - server.addConnectFuture(serverConnFut); - - URI wsUri = server.getWsUri(); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - request.setSubProtocols("echo"); - Future future = client.connect(cliSock,wsUri,request); - - try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) - { - // Setup echo of frames on server side - serverConn.setIncomingFrameConsumer((frame)->{ - WebSocketFrame copy = WebSocketFrame.copy(frame); - copy.setMask(null); // strip client mask (if present) - serverConn.write(copy); - }); - - Session sess = future.get(30,TimeUnit.SECONDS); - assertThat("Session",sess,notNullValue()); - assertThat("Session.open",sess.isOpen(),is(true)); - assertThat("Session.upgradeRequest",sess.getUpgradeRequest(),notNullValue()); - assertThat("Session.upgradeResponse",sess.getUpgradeResponse(),notNullValue()); - - cliSock.assertWasOpened(); - cliSock.assertNotClosed(); - - Collection sessions = client.getOpenSessions(); - assertThat("client.connectionManager.sessions.size",sessions.size(),is(1)); - - RemoteEndpoint remote = cliSock.getSession().getRemote(); - remote.sendStringByFuture("Hello World!"); - if (remote.getBatchMode() == BatchMode.ON) - remote.flush(); - - // wait for response from server - String received = cliSock.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); - assertThat("Message", received, containsString("Hello World")); - } - } - - @Test - public void testBasicEcho_UsingCallback() throws Exception - { - client.setMaxIdleTimeout(160000); - JettyTrackingSocket cliSock = new JettyTrackingSocket(); - - // Hook into server connection creation - CompletableFuture serverConnFut = new CompletableFuture<>(); - server.addConnectFuture(serverConnFut); - - URI wsUri = server.getWsUri(); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - request.setSubProtocols("echo"); - Future future = client.connect(cliSock,wsUri,request); - - try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) - { - Session sess = future.get(30, TimeUnit.SECONDS); - assertThat("Session", sess, notNullValue()); - assertThat("Session.open", sess.isOpen(), is(true)); - assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); - assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); - - cliSock.assertWasOpened(); - cliSock.assertNotClosed(); - - Collection sessions = client.getBeans(WebSocketSession.class); - assertThat("client.connectionManager.sessions.size", sessions.size(), is(1)); - - FutureWriteCallback callback = new FutureWriteCallback(); - - cliSock.getSession().getRemote().sendString("Hello World!", callback); - callback.get(1, TimeUnit.SECONDS); - } - } - - @Test - public void testBasicEcho_FromServer() throws Exception - { - // Hook into server connection creation - CompletableFuture serverConnFut = new CompletableFuture<>(); - server.addConnectFuture(serverConnFut); - - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - Future future = client.connect(wsocket,server.getWsUri()); - - try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) - { - // Validate connect - Session sess = future.get(30, TimeUnit.SECONDS); - assertThat("Session", sess, notNullValue()); - assertThat("Session.open", sess.isOpen(), is(true)); - assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); - assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); - - // Have server send initial message - serverConn.write(new TextFrame().setPayload("Hello World")); - - // Verify connect - future.get(30, TimeUnit.SECONDS); - wsocket.assertWasOpened(); - - String received = wsocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); - assertThat("Message", received, containsString("Hello World")); - } - } - - @Test - public void testLocalRemoteAddress() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - future.get(30,TimeUnit.SECONDS); - - assertTrue(wsocket.openLatch.await(1,TimeUnit.SECONDS)); - - InetSocketAddress local = wsocket.getSession().getLocalAddress(); - InetSocketAddress remote = wsocket.getSession().getRemoteAddress(); - - assertThat("Local Socket Address",local,notNullValue()); - assertThat("Remote Socket Address",remote,notNullValue()); - - // Hard to validate (in a portable unit test) the local address that was used/bound in the low level Jetty Endpoint - assertThat("Local Socket Address / Host",local.getAddress().getHostAddress(),notNullValue()); - assertThat("Local Socket Address / Port",local.getPort(),greaterThan(0)); - - String uriHostAddress = InetAddress.getByName(wsUri.getHost()).getHostAddress(); - assertThat("Remote Socket Address / Host",remote.getAddress().getHostAddress(),is(uriHostAddress)); - assertThat("Remote Socket Address / Port",remote.getPort(),greaterThan(0)); - } - - /** - * Ensure that @WebSocket(maxTextMessageSize = 100*1024) behaves as expected. - * - * @throws Exception - * on test failure - */ - @Test - public void testMaxMessageSize() throws Exception - { - MaxMessageSocket wsocket = new MaxMessageSocket(); - - // Hook into server connection creation - CompletableFuture serverConnFut = new CompletableFuture<>(); - server.addConnectFuture(serverConnFut); - - URI wsUri = server.getWsUri(); - Future future = client.connect(wsocket,wsUri); - - try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) - { - // Setup echo of frames on server side - serverConn.setIncomingFrameConsumer((frame)->{ - WebSocketFrame copy = WebSocketFrame.copy(frame); - copy.setMask(null); // strip client mask (if present) - serverConn.write(copy); - }); - - wsocket.awaitConnect(1,TimeUnit.SECONDS); - - Session sess = future.get(30,TimeUnit.SECONDS); - assertThat("Session",sess,notNullValue()); - assertThat("Session.open",sess.isOpen(),is(true)); - - // Create string that is larger than default size of 64k - // but smaller than maxMessageSize of 100k - byte buf[] = new byte[80 * 1024]; - Arrays.fill(buf,(byte)'x'); - String msg = StringUtil.toUTF8String(buf,0,buf.length); - - wsocket.getSession().getRemote().sendStringByFuture(msg); - - // wait for response from server - wsocket.waitForMessage(1, TimeUnit.SECONDS); - - wsocket.assertMessage(msg); - - assertTrue(wsocket.dataLatch.await(2, TimeUnit.SECONDS)); - } - } - - @Test - public void testParameterMap() throws Exception - { - JettyTrackingSocket wsocket = new JettyTrackingSocket(); - - URI wsUri = server.getWsUri().resolve("/test?snack=cashews&amount=handful&brand=off"); - Future future = client.connect(wsocket,wsUri); - - future.get(30,TimeUnit.SECONDS); - - assertTrue(wsocket.openLatch.await(1,TimeUnit.SECONDS)); - - Session session = wsocket.getSession(); - UpgradeRequest req = session.getUpgradeRequest(); - assertThat("Upgrade Request",req,notNullValue()); - - Map> parameterMap = req.getParameterMap(); - assertThat("Parameter Map",parameterMap,notNullValue()); - - assertThat("Parameter[snack]",parameterMap.get("snack"),is(Arrays.asList(new String[] { "cashews" }))); - assertThat("Parameter[amount]",parameterMap.get("amount"),is(Arrays.asList(new String[] { "handful" }))); - assertThat("Parameter[brand]",parameterMap.get("brand"),is(Arrays.asList(new String[] { "off" }))); - - assertThat("Parameter[cost]",parameterMap.get("cost"),nullValue()); - } -} diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/NativeWebSocketServletContainerInitializer.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/NativeWebSocketServletContainerInitializer.java index 4559ae1fdb7..7853b548fc3 100644 --- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/NativeWebSocketServletContainerInitializer.java +++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/NativeWebSocketServletContainerInitializer.java @@ -19,40 +19,98 @@ package org.eclipse.jetty.websocket.server; import java.util.Set; - import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.listener.ContainerInitializer; public class NativeWebSocketServletContainerInitializer implements ServletContainerInitializer { + public static final String ATTR_KEY = NativeWebSocketConfiguration.class.getName(); + + public interface Configurator + { + void accept(ServletContext servletContext, NativeWebSocketConfiguration nativeWebSocketConfiguration); + } + + /** + * Initialize the ServletContext with the default (and empty) {@link NativeWebSocketConfiguration} + * + * @param context the context to work with + */ + public static void initialize(ServletContext context) + { + NativeWebSocketConfiguration configuration = (NativeWebSocketConfiguration)context.getAttribute(ATTR_KEY); + if (configuration != null) + return; // it exists. + + // Not provided to us, create a new default one. + configuration = new NativeWebSocketConfiguration(context); + context.setAttribute(ATTR_KEY, configuration); + + // Attach default configuration to context lifecycle + if (context instanceof ContextHandler.Context) + { + ContextHandler handler = ((ContextHandler.Context)context).getContextHandler(); + // Let ContextHandler handle configuration lifecycle + handler.addManaged(configuration); + } + } + + /** + * Configure the {@link ServletContextHandler} to call the {@link NativeWebSocketServletContainerInitializer} + * during the {@link ServletContext} initialization phase. + * + * @param context the context to add listener to. + */ + public static void configure(ServletContextHandler context) + { + context.addEventListener(ContainerInitializer.asContextListener(new NativeWebSocketServletContainerInitializer())); + } + + /** + * Configure the {@link ServletContextHandler} to call the {@link NativeWebSocketServletContainerInitializer} + * during the {@link ServletContext} initialization phase. + * + * @param context the context to add listener to. + * @param configurator a lambda that is called to allow the {@link NativeWebSocketConfiguration} to + * be configured during {@link ServletContext} initialization phase + */ + public static void configure(ServletContextHandler context, Configurator configurator) + { + context.addEventListener( + ContainerInitializer + .asContextListener(new NativeWebSocketServletContainerInitializer()) + .setPostOnStartupConsumer((servletContext) -> + { + NativeWebSocketConfiguration configuration = (NativeWebSocketConfiguration)servletContext.getAttribute(ATTR_KEY); + configurator.accept(servletContext, configuration); + })); + } + + /** + * Obtain the default {@link NativeWebSocketConfiguration} from the {@link ServletContext} + * + * @param context the context to work with + * @return the default {@link NativeWebSocketConfiguration} + * @see #initialize(ServletContext) + * @see #configure(ServletContextHandler) + * @see #configure(ServletContextHandler, Configurator) + * @deprecated use {@link #configure(ServletContextHandler, Configurator)} instead + */ + @Deprecated public static NativeWebSocketConfiguration getDefaultFrom(ServletContext context) { - final String KEY = NativeWebSocketConfiguration.class.getName(); - - NativeWebSocketConfiguration configuration = (NativeWebSocketConfiguration) context.getAttribute(KEY); - if (configuration == null) - { - // Not provided to us, create a new default one. - configuration = new NativeWebSocketConfiguration(context); - context.setAttribute(KEY, configuration); - - // Attach default configuration to context lifecycle - if (context instanceof ContextHandler.Context) - { - ContextHandler handler = ((ContextHandler.Context)context).getContextHandler(); - // Let ContextHandler handle configuration lifecycle - handler.addManaged(configuration); - } - } - return configuration; + initialize(context); + return (NativeWebSocketConfiguration)context.getAttribute(ATTR_KEY); } - + @Override public void onStartup(Set> c, ServletContext ctx) { // initialize - getDefaultFrom(ctx); + initialize(ctx); } }