Merged branch 'jetty-10.0.x' into 'jetty-11.0.x'.

This commit is contained in:
Simone Bordet 2020-11-23 16:59:59 +01:00
commit 8a37363a3f
14 changed files with 508 additions and 105 deletions

View File

@ -1084,6 +1084,23 @@ public class ServletHandler extends ScopedHandler
}
}
/**
* Convenience method to add a preconstructed FilterHolder
*
* @param filter the filter holder
*/
public void prependFilter(FilterHolder filter)
{
if (filter == null)
return;
try (AutoLock l = lock())
{
if (!containsFilterHolder(filter))
setFilters(ArrayUtil.prependToArray(filter, getFilters(), FilterHolder.class));
}
}
/**
* Convenience method to add a preconstructed FilterMapping
*

View File

@ -40,7 +40,7 @@ import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.jakarta.server.internal.JakartaWebSocketServerContainer;
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMappings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -153,7 +153,7 @@ public class JakartaWebSocketServletContainerInitializer implements ServletConta
{
WebSocketComponents components = WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), context.getServletContext());
FilterHolder filterHolder = WebSocketUpgradeFilter.ensureFilter(context.getServletContext());
WebSocketMapping mapping = WebSocketMapping.ensureMapping(context.getServletContext(), WebSocketMapping.DEFAULT_KEY);
WebSocketMappings mapping = WebSocketMappings.ensureMapping(context.getServletContext());
serverContainer = JakartaWebSocketServerContainer.ensureContainer(context.getServletContext());
if (LOG.isDebugEnabled())

View File

@ -43,7 +43,7 @@ import org.eclipse.jetty.websocket.jakarta.server.config.ContainerDefaultConfigu
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.util.InvalidSignatureException;
import org.eclipse.jetty.websocket.util.ReflectUtils;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMappings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -99,7 +99,7 @@ public class JakartaWebSocketServerContainer extends JakartaWebSocketClientConta
// Create the Jetty ServerContainer implementation
container = new JakartaWebSocketServerContainer(
WebSocketMapping.ensureMapping(servletContext, WebSocketMapping.DEFAULT_KEY),
WebSocketMappings.ensureMapping(servletContext),
WebSocketServerComponents.getWebSocketComponents(servletContext),
coreClientSupplier);
contextHandler.addManaged(container);
@ -110,7 +110,7 @@ public class JakartaWebSocketServerContainer extends JakartaWebSocketClientConta
return container;
}
private final WebSocketMapping webSocketMapping;
private final WebSocketMappings webSocketMappings;
private final JakartaWebSocketServerFrameHandlerFactory frameHandlerFactory;
private List<Class<?>> deferredEndpointClasses;
private List<ServerEndpointConfig> deferredEndpointConfigs;
@ -118,31 +118,31 @@ public class JakartaWebSocketServerContainer extends JakartaWebSocketClientConta
/**
* Main entry point for {@link JakartaWebSocketServletContainerInitializer}.
*
* @param webSocketMapping the {@link WebSocketMapping} that this container belongs to
* @param webSocketMappings the {@link WebSocketMappings} that this container belongs to
*/
public JakartaWebSocketServerContainer(WebSocketMapping webSocketMapping)
public JakartaWebSocketServerContainer(WebSocketMappings webSocketMappings)
{
this(webSocketMapping, new WebSocketComponents());
this(webSocketMappings, new WebSocketComponents());
}
public JakartaWebSocketServerContainer(WebSocketMapping webSocketMapping, WebSocketComponents components)
public JakartaWebSocketServerContainer(WebSocketMappings webSocketMappings, WebSocketComponents components)
{
super(components);
this.webSocketMapping = webSocketMapping;
this.webSocketMappings = webSocketMappings;
this.frameHandlerFactory = new JakartaWebSocketServerFrameHandlerFactory(this);
}
/**
* Main entry point for {@link JakartaWebSocketServletContainerInitializer}.
*
* @param webSocketMapping the {@link WebSocketMapping} that this container belongs to
* @param webSocketMappings the {@link WebSocketMappings} that this container belongs to
* @param components the {@link WebSocketComponents} instance to use
* @param coreClientSupplier the supplier of the {@link WebSocketCoreClient} instance to use
*/
public JakartaWebSocketServerContainer(WebSocketMapping webSocketMapping, WebSocketComponents components, Function<WebSocketComponents, WebSocketCoreClient> coreClientSupplier)
public JakartaWebSocketServerContainer(WebSocketMappings webSocketMappings, WebSocketComponents components, Function<WebSocketComponents, WebSocketCoreClient> coreClientSupplier)
{
super(components, coreClientSupplier);
this.webSocketMapping = webSocketMapping;
this.webSocketMappings = webSocketMappings;
this.frameHandlerFactory = new JakartaWebSocketServerFrameHandlerFactory(this);
}
@ -265,7 +265,7 @@ public class JakartaWebSocketServerContainer extends JakartaWebSocketClientConta
frameHandlerFactory.getMetadata(config.getEndpointClass(), config);
JakartaWebSocketCreator creator = new JakartaWebSocketCreator(this, config, getExtensionRegistry());
PathSpec pathSpec = new UriTemplatePathSpec(config.getPath());
webSocketMapping.addMapping(pathSpec, creator, frameHandlerFactory, defaultCustomizer);
webSocketMappings.addMapping(pathSpec, creator, frameHandlerFactory, defaultCustomizer);
}
catch (InvalidSignatureException e)
{

View File

@ -61,7 +61,7 @@ public class AltFilterTest
{
wsb.start();
FilterHolder filterWebXml = app.getWebAppContext().getServletHandler().getFilter("wsuf-test");
FilterHolder filterWebXml = app.getWebAppContext().getServletHandler().getFilter(WebSocketUpgradeFilter.class.getName());
assertThat("Filter[wsuf-test]", filterWebXml, notNullValue());
FilterHolder filterSCI = app.getWebAppContext().getServletHandler().getFilter("Jetty_WebSocketUpgradeFilter");

View File

@ -12,16 +12,12 @@
</context-param>
<filter>
<filter-name>wsuf-test</filter-name>
<filter-name>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-name>
<filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class>
<init-param>
<param-name>jetty.websocket.WebSocketMapping</param-name>
<param-value>jetty.websocket.defaultMapping</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>wsuf-test</filter-name>
<filter-name>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-name>
<url-pattern>/echo/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -47,7 +47,7 @@ import org.eclipse.jetty.websocket.server.internal.JettyServerFrameHandlerFactor
import org.eclipse.jetty.websocket.util.ReflectUtils;
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMappings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -77,7 +77,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
// Create the Jetty ServerContainer implementation
container = new JettyWebSocketServerContainer(
contextHandler,
WebSocketMapping.ensureMapping(servletContext, WebSocketMapping.DEFAULT_KEY),
WebSocketMappings.ensureMapping(servletContext),
WebSocketServerComponents.getWebSocketComponents(servletContext), executor);
servletContext.setAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE, container);
contextHandler.addManaged(container);
@ -90,7 +90,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
private static final Logger LOG = LoggerFactory.getLogger(JettyWebSocketServerContainer.class);
private final ServletContextHandler contextHandler;
private final WebSocketMapping webSocketMapping;
private final WebSocketMappings webSocketMappings;
private final WebSocketComponents components;
private final FrameHandlerFactory frameHandlerFactory;
private final Executor executor;
@ -102,14 +102,14 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
/**
* Main entry point for {@link JettyWebSocketServletContainerInitializer}.
*
* @param webSocketMapping the {@link WebSocketMapping} that this container belongs to
* @param webSocketMappings the {@link WebSocketMappings} that this container belongs to
* @param components the {@link WebSocketComponents} instance to use
* @param executor the {@link Executor} to use
*/
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents components, Executor executor)
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMappings webSocketMappings, WebSocketComponents components, Executor executor)
{
this.contextHandler = contextHandler;
this.webSocketMapping = webSocketMapping;
this.webSocketMappings = webSocketMappings;
this.executor = executor;
this.components = components;
@ -129,12 +129,12 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
public void addMapping(String pathSpec, JettyWebSocketCreator creator)
{
PathSpec ps = WebSocketMapping.parsePathSpec(pathSpec);
if (webSocketMapping.getMapping(ps) != null)
PathSpec ps = WebSocketMappings.parsePathSpec(pathSpec);
if (webSocketMappings.getMapping(ps) != null)
throw new WebSocketException("Duplicate WebSocket Mapping for PathSpec");
WebSocketUpgradeFilter.ensureFilter(contextHandler.getServletContext());
webSocketMapping.addMapping(ps,
webSocketMappings.addMapping(ps,
(req, resp) -> creator.createWebSocket(new DelegatedServerUpgradeRequest(req), new DelegatedServerUpgradeResponse(resp)),
frameHandlerFactory, customizer);
}

View File

@ -40,14 +40,14 @@ import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory;
import org.eclipse.jetty.websocket.util.server.internal.ServerUpgradeRequest;
import org.eclipse.jetty.websocket.util.server.internal.ServerUpgradeResponse;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketCreator;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMappings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract Servlet used to bridge the Servlet API to the WebSocket API.
* <p>
* To use this servlet, you will be required to register your websockets with the {@link WebSocketMapping} so that it can create your websockets under the
* To use this servlet, you will be required to register your websockets with the {@link WebSocketMappings} so that it can create your websockets under the
* appropriate conditions.
* </p>
* <p>The most basic implementation would be as follows:</p>
@ -68,11 +68,11 @@ import org.slf4j.LoggerFactory;
* }
* </pre>
* <p>
* Only request that conforms to a "WebSocket: Upgrade" handshake request will trigger the {@link WebSocketMapping} handling of creating
* Only request that conforms to a "WebSocket: Upgrade" handshake request will trigger the {@link WebSocketMappings} handling of creating
* WebSockets. All other requests are treated as normal servlet requests. The configuration defined by this servlet init parameters will
* be used as the customizer for any mappings created by {@link JettyWebSocketServletFactory#addMapping(String, JettyWebSocketCreator)} during
* {@link #configure(JettyWebSocketServletFactory)} calls. The request upgrade may be peformed by this servlet, or is may be performed by a
* {@link WebSocketUpgradeFilter} instance that will share the same {@link WebSocketMapping} instance. If the filter is used, then the
* {@link WebSocketUpgradeFilter} instance that will share the same {@link WebSocketMappings} instance. If the filter is used, then the
* filter configuraton is used as the default configuration prior to this servlets configuration being applied.
* </p>
* <p>
@ -99,7 +99,7 @@ public abstract class JettyWebSocketServlet extends HttpServlet
private static final Logger LOG = LoggerFactory.getLogger(JettyWebSocketServlet.class);
private final CustomizedWebSocketServletFactory customizer = new CustomizedWebSocketServletFactory();
private WebSocketMapping mapping;
private WebSocketMappings mapping;
private WebSocketComponents components;
/**
@ -133,7 +133,7 @@ public abstract class JettyWebSocketServlet extends HttpServlet
ServletContext servletContext = getServletContext();
components = WebSocketServerComponents.getWebSocketComponents(servletContext);
mapping = new WebSocketMapping(components);
mapping = new WebSocketMappings(components);
String max = getInitParameter("idleTimeout");
if (max == null)
@ -208,7 +208,7 @@ public abstract class JettyWebSocketServlet extends HttpServlet
@Override
public void addMapping(String pathSpec, JettyWebSocketCreator creator)
{
mapping.addMapping(WebSocketMapping.parsePathSpec(pathSpec), new WrappedJettyCreator(creator), getFactory(), this);
mapping.addMapping(WebSocketMappings.parsePathSpec(pathSpec), new WrappedJettyCreator(creator), getFactory(), this);
}
@Override
@ -249,7 +249,7 @@ public abstract class JettyWebSocketServlet extends HttpServlet
@Override
public JettyWebSocketCreator getMapping(String pathSpec)
{
WebSocketCreator creator = mapping.getMapping(WebSocketMapping.parsePathSpec(pathSpec));
WebSocketCreator creator = mapping.getMapping(WebSocketMappings.parsePathSpec(pathSpec));
if (creator instanceof WrappedJettyCreator)
return ((WrappedJettyCreator)creator).getJettyWebSocketCreator();
@ -259,7 +259,7 @@ public abstract class JettyWebSocketServlet extends HttpServlet
@Override
public boolean removeMapping(String pathSpec)
{
return mapping.removeMapping(WebSocketMapping.parsePathSpec(pathSpec));
return mapping.removeMapping(WebSocketMappings.parsePathSpec(pathSpec));
}
}

View File

@ -27,7 +27,7 @@ import org.eclipse.jetty.servlet.listener.ContainerInitializer;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMappings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -48,7 +48,7 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain
* 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 WebSocketMapping} to
* @param configurator a lambda that is called to allow the {@link WebSocketMappings} to
* be configured during {@link ServletContext} initialization phase
*/
public static void configure(ServletContextHandler context, Configurator configurator)

View File

@ -18,29 +18,50 @@
package org.eclipse.jetty.websocket.tests;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpServlet;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.core.WebSocketConstants;
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
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.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class JettyWebSocketFilterTest
@ -50,6 +71,16 @@ public class JettyWebSocketFilterTest
private WebSocketClient client;
private ServletContextHandler contextHandler;
@BeforeEach
public void before()
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
client = new WebSocketClient();
}
public void start(JettyWebSocketServletContainerInitializer.Configurator configurator) throws Exception
{
start(configurator, null);
@ -62,20 +93,14 @@ public class JettyWebSocketFilterTest
public void start(JettyWebSocketServletContainerInitializer.Configurator configurator, ServletHolder servletHolder) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
if (servletHolder != null)
contextHandler.addServlet(servletHolder, "/");
server.setHandler(contextHandler);
JettyWebSocketServletContainerInitializer.configure(contextHandler, configurator);
server.start();
client = new WebSocketClient();
server.start();
client.start();
}
@ -93,7 +118,7 @@ public class JettyWebSocketFilterTest
// After mapping is added we have an UpgradeFilter.
assertThat(contextHandler.getServletHandler().getFilters().length, is(1));
FilterHolder filterHolder = contextHandler.getServletHandler().getFilter("WebSocketUpgradeFilter");
FilterHolder filterHolder = contextHandler.getServletHandler().getFilter(WebSocketUpgradeFilter.class.getName());
assertNotNull(filterHolder);
assertThat(filterHolder.getState(), is(AbstractLifeCycle.STARTED));
assertThat(filterHolder.getFilter(), instanceOf(WebSocketUpgradeFilter.class));
@ -127,7 +152,7 @@ public class JettyWebSocketFilterTest
// After mapping is added we have an UpgradeFilter.
container.addMapping("/", EchoSocket.class);
assertThat(contextHandler.getServletHandler().getFilters().length, is(1));
FilterHolder filterHolder = contextHandler.getServletHandler().getFilter("WebSocketUpgradeFilter");
FilterHolder filterHolder = contextHandler.getServletHandler().getFilter(WebSocketUpgradeFilter.class.getName());
assertNotNull(filterHolder);
assertThat(filterHolder.getState(), is(AbstractLifeCycle.STARTED));
assertThat(filterHolder.getFilter(), instanceOf(WebSocketUpgradeFilter.class));
@ -164,7 +189,7 @@ public class JettyWebSocketFilterTest
// After mapping is added we have an UpgradeFilter.
assertThat(contextHandler.getServletHandler().getFilters().length, is(1));
FilterHolder filterHolder = contextHandler.getServletHandler().getFilter("WebSocketUpgradeFilter");
FilterHolder filterHolder = contextHandler.getServletHandler().getFilter(WebSocketUpgradeFilter.class.getName());
assertNotNull(filterHolder);
assertThat(filterHolder.getState(), is(AbstractLifeCycle.STARTED));
assertThat(filterHolder.getFilter(), instanceOf(WebSocketUpgradeFilter.class));
@ -182,4 +207,233 @@ public class JettyWebSocketFilterTest
String msg = socket.textMessages.poll();
assertThat(msg, is("hello world"));
}
@Test
public void testMultipleWebSocketUpgradeFilter() throws Exception
{
String idleTimeoutFilter1 = "4999";
String idleTimeoutFilter2 = "3999";
start((context, container) ->
{
ServletContextHandler contextHandler = Objects.requireNonNull(ServletContextHandler.getServletContextHandler(context));
// This filter replaces the default filter as we use the pre-defined name.
FilterHolder filterHolder = new FilterHolder(WebSocketUpgradeFilter.class);
filterHolder.setName(WebSocketUpgradeFilter.class.getName());
filterHolder.setInitParameter("idleTimeout", idleTimeoutFilter1);
contextHandler.addFilter(filterHolder, "/primaryFilter/*", EnumSet.of(DispatcherType.REQUEST));
// This is an additional filter.
filterHolder = new FilterHolder(WebSocketUpgradeFilter.class);
filterHolder.setName("Secondary Upgrade Filter");
filterHolder.setInitParameter("idleTimeout", idleTimeoutFilter2);
contextHandler.addFilter(filterHolder, "/secondaryFilter/*", EnumSet.of(DispatcherType.REQUEST));
// Add mappings to the server container (same WebSocketMappings is referenced by both upgrade filters).
container.addMapping("/echo", EchoSocket.class);
container.addMapping("/primaryFilter/echo", LowerCaseEchoSocket.class);
container.addMapping("/secondaryFilter/echo", UpperCaseEchoSocket.class);
});
// Verify we have manually added 2 WebSocketUpgrade Filters.
List<FilterHolder> upgradeFilters = Arrays.stream(contextHandler.getServletHandler().getFilters())
.filter(holder -> holder.getFilter() instanceof WebSocketUpgradeFilter)
.collect(Collectors.toList());
assertThat(contextHandler.getServletHandler().getFilters().length, is(2));
assertThat(upgradeFilters.size(), is(2));
for (FilterHolder filterHolder : upgradeFilters)
{
assertThat(filterHolder.getState(), is(AbstractLifeCycle.STARTED));
assertThat(filterHolder.getFilter(), instanceOf(WebSocketUpgradeFilter.class));
}
// The /echo path should not match either of the upgrade filters even though it has a valid mapping, we get 404 response.
URI firstUri = URI.create("ws://localhost:" + connector.getLocalPort() + "/echo");
ExecutionException error = assertThrows(ExecutionException.class, () -> client.connect(new EventSocket(), firstUri).get(5, TimeUnit.SECONDS));
assertThat(error.getMessage(), containsString("404 Not Found"));
// The /primaryFilter/echo path should convert to lower case and have idleTimeout configured on the first upgradeFilter.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/primaryFilter/echo");
EventSocket socket = new EventSocket();
CompletableFuture<Session> connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hElLo wOrLd");
session.getRemote().sendString("getIdleTimeout");
}
assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(socket.textMessages.poll(), is("hello world"));
assertThat(socket.textMessages.poll(), is(idleTimeoutFilter1));
// The /secondaryFilter/echo path should convert to upper case and have idleTimeout configured on the second upgradeFilter.
uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/secondaryFilter/echo");
socket = new EventSocket();
connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hElLo wOrLd");
session.getRemote().sendString("getIdleTimeout");
}
assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(socket.textMessages.poll(), is("HELLO WORLD"));
assertThat(socket.textMessages.poll(), is(idleTimeoutFilter2));
}
@Test
public void testCustomUpgradeFilter() throws Exception
{
start((context, container) ->
{
ServletContextHandler contextHandler = Objects.requireNonNull(ServletContextHandler.getServletContextHandler(context));
// This custom filter replaces the default filter as we use the pre-defined name, and adds mapping in init().
FilterHolder filterHolder = new FilterHolder(MyUpgradeFilter.class);
filterHolder.setName(WebSocketUpgradeFilter.class.getName());
contextHandler.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
});
FilterHolder[] holders = contextHandler.getServletHandler().getFilters();
assertThat(holders.length, is(1));
assertThat(holders[0].getFilter(), instanceOf(MyUpgradeFilter.class));
// We can reach the echo endpoint and get correct response.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/echo");
EventSocket socket = new EventSocket();
CompletableFuture<Session> connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hElLo wOrLd");
}
assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(socket.textMessages.poll(), is("hElLo wOrLd"));
}
@Test
public void testDefaultWebSocketUpgradeFilterOrdering() throws Exception
{
String defaultIdleTimeout = Long.toString(WebSocketConstants.DEFAULT_IDLE_TIMEOUT.toMillis());
JettyWebSocketWebApp webApp = new JettyWebSocketWebApp("wsuf-ordering1");
Path webXml = MavenTestingUtils.getTestResourcePath("wsuf-ordering1.xml");
webApp.copyWebXml(webXml);
webApp.copyClass(WebSocketEchoServletContextListener.class);
webApp.copyClass(WebSocketEchoServletContextListener.EchoSocket.class);
server.setHandler(webApp);
server.start();
client.start();
// We have both websocket upgrade filters installed.
FilterHolder[] filterHolders = webApp.getServletHandler().getFilters();
assertThat(filterHolders.length, is(2));
assertThat(filterHolders[0].getFilter(), instanceOf(WebSocketUpgradeFilter.class));
assertThat(filterHolders[1].getFilter(), instanceOf(WebSocketUpgradeFilter.class));
// The custom filter defined in web.xml should be first in line so it will do the upgrade.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + webApp.getContextPath() + "/echo");
EventSocket socket = new EventSocket();
CompletableFuture<Session> connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hello world");
session.getRemote().sendString("getIdleTimeout");
}
assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(socket.textMessages.poll(), is("hello world"));
assertThat(socket.textMessages.poll(), is(defaultIdleTimeout));
}
@Test
public void testWebSocketUpgradeFilterOrdering() throws Exception
{
String timeoutFromAltFilter = "5999";
JettyWebSocketWebApp webApp = new JettyWebSocketWebApp("wsuf-ordering2");
Path webXml = MavenTestingUtils.getTestResourcePath("wsuf-ordering2.xml");
webApp.copyWebXml(webXml);
webApp.copyClass(WebSocketEchoServletContextListener.class);
webApp.copyClass(WebSocketEchoServletContextListener.EchoSocket.class);
server.setHandler(webApp);
server.start();
client.start();
// We have both websocket upgrade filters installed.
FilterHolder[] filterHolders = webApp.getServletHandler().getFilters();
assertThat(filterHolders.length, is(2));
assertThat(filterHolders[0].getFilter(), instanceOf(WebSocketUpgradeFilter.class));
assertThat(filterHolders[1].getFilter(), instanceOf(WebSocketUpgradeFilter.class));
// The custom filter defined in web.xml should be first in line so it will do the upgrade.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + webApp.getContextPath() + "/echo");
EventSocket socket = new EventSocket();
CompletableFuture<Session> connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hello world");
session.getRemote().sendString("getIdleTimeout");
}
assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(socket.textMessages.poll(), is("hello world"));
assertThat(socket.textMessages.poll(), is(timeoutFromAltFilter));
}
@WebListener
public static class WebSocketEchoServletContextListener implements ServletContextListener
{
@Override
public void contextInitialized(ServletContextEvent sce)
{
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.getContainer(sce.getServletContext());
container.addMapping("/echo", EchoSocket.class);
}
@WebSocket
public static class EchoSocket
{
@OnWebSocketMessage
public void onMessage(Session session, String message) throws IOException
{
if ("getIdleTimeout".equals(message))
session.getRemote().sendString(Long.toString(session.getIdleTimeout().toMillis()));
else
session.getRemote().sendString(message);
}
}
}
public static class MyUpgradeFilter extends WebSocketUpgradeFilter
{
@Override
public void init(FilterConfig config) throws ServletException
{
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.getContainer(config.getServletContext());
container.addMapping("/echo", EchoSocket.class);
super.init(config);
}
}
@WebSocket
public static class LowerCaseEchoSocket
{
@OnWebSocketMessage
public void onMessage(Session session, String message) throws IOException
{
if ("getIdleTimeout".equals(message))
session.getRemote().sendString(Long.toString(session.getIdleTimeout().toMillis()));
else
session.getRemote().sendString(message.toLowerCase());
}
}
@WebSocket
public static class UpperCaseEchoSocket
{
@OnWebSocketMessage
public void onMessage(Session session, String message) throws IOException
{
if ("getIdleTimeout".equals(message))
session.getRemote().sendString(Long.toString(session.getIdleTimeout().toMillis()));
else
session.getRemote().sendString(message.toUpperCase());
}
}
}

View File

@ -0,0 +1,102 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.tests;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.IO;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
public class JettyWebSocketWebApp extends WebAppContext
{
private static final Logger LOG = LoggerFactory.getLogger(JettyWebSocketWebApp.class);
private final Path contextDir;
private final Path webInf;
private final Path classesDir;
public JettyWebSocketWebApp(String contextName)
{
// Ensure context directory.
Path testDir = MavenTestingUtils.getTargetTestingPath(JettyWebSocketWebApp.class.getName());
contextDir = testDir.resolve(contextName);
FS.ensureEmpty(contextDir);
// Ensure WEB-INF directories.
webInf = contextDir.resolve("WEB-INF");
FS.ensureDirExists(webInf);
classesDir = webInf.resolve("classes");
FS.ensureDirExists(classesDir);
// Configure the WebAppContext.
setContextPath("/" + contextName);
setBaseResource(new PathResource(contextDir));
addConfiguration(new JettyWebSocketConfiguration());
}
public Path getContextDir()
{
return contextDir;
}
public void createWebXml() throws IOException
{
String emptyWebXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<web-app xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://java.sun.com/xml/ns/javaee\" " +
"xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd\" " +
"metadata-complete=\"false\" version=\"3.0\"></web-app>";
Path webXml = webInf.resolve("web.xml");
try (FileWriter writer = new FileWriter(webXml.toFile()))
{
writer.write(emptyWebXml);
}
}
public void copyWebXml(Path webXml) throws IOException
{
IO.copy(webXml.toFile(), webInf.resolve("web.xml").toFile());
}
public void copyClass(Class<?> clazz) throws Exception
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String endpointPath = TypeUtil.toClassReference(clazz);
URL classUrl = cl.getResource(endpointPath);
assertThat("Class URL for: " + clazz, classUrl, notNullValue());
Path destFile = classesDir.resolve(endpointPath);
FS.ensureDirExists(destFile.getParent());
File srcFile = new File(classUrl.toURI());
IO.copy(srcFile, destFile.toFile());
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
metadata-complete="false"
version="4.0">
<filter>
<filter-name>wsuf-alt</filter-name>
<filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class>
<init-param>
<param-name>idleTimeout</param-name>
<param-value>5999</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>wsuf-alt</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
metadata-complete="false"
version="4.0">
<!-- Add the custom filter first. -->
<filter>
<filter-name>wsuf-alt</filter-name>
<filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class>
<init-param>
<param-name>idleTimeout</param-name>
<param-value>5999</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>wsuf-alt</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- The default filter is overridden so it is tried after. -->
<filter>
<filter-name>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-name>
<filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -35,12 +35,13 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.FilterMapping;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMappings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -49,7 +50,7 @@ import org.slf4j.LoggerFactory;
* <p>
* The configuration applied to this filter via init params will be used as the the default
* configuration of any websocket upgraded by this filter, prior to the configuration of the
* websocket applied by the {@link WebSocketMapping}.
* websocket applied by the {@link WebSocketMappings}.
* </p>
* <p>
* <b>Configuration / Init-Parameters:</b>
@ -78,26 +79,16 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
private static final AutoLock LOCK = new AutoLock();
/**
* The init parameter name used to define {@link ServletContext} attribute used to share the {@link WebSocketMapping}.
*/
public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "jetty.websocket.WebSocketMapping";
/**
* Return any {@link WebSocketUpgradeFilter} already present on the {@link ServletContext}.
* Return the default {@link WebSocketUpgradeFilter} if present on the {@link ServletContext}.
*
* @param servletContext the {@link ServletContext} to use.
* @return the configured default {@link WebSocketUpgradeFilter} instance.
*/
private static FilterHolder getFilter(ServletContext servletContext)
public static FilterHolder getFilter(ServletContext servletContext)
{
ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext));
ServletHandler servletHandler = contextHandler.getChildHandlerByClass(ServletHandler.class);
for (FilterHolder holder : servletHandler.getFilters())
{
if (holder.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM) != null)
return holder;
}
return null;
return servletHandler.getFilter(WebSocketUpgradeFilter.class.getName());
}
/**
@ -120,16 +111,23 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
if (existingFilter != null)
return existingFilter;
final String name = "WebSocketUpgradeFilter";
final String pathSpec = "/*";
FilterHolder holder = new FilterHolder(new WebSocketUpgradeFilter());
holder.setName(name);
holder.setInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM, WebSocketMapping.DEFAULT_KEY);
holder.setAsyncSupported(true);
ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext));
ServletHandler servletHandler = contextHandler.getChildHandlerByClass(ServletHandler.class);
servletHandler.addFilterWithMapping(holder, pathSpec, EnumSet.of(DispatcherType.REQUEST));
final String pathSpec = "/*";
FilterHolder holder = new FilterHolder(new WebSocketUpgradeFilter());
holder.setName(WebSocketUpgradeFilter.class.getName());
holder.setAsyncSupported(true);
FilterMapping mapping = new FilterMapping();
mapping.setFilterName(holder.getName());
mapping.setPathSpec(pathSpec);
mapping.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
// Add the default WebSocketUpgradeFilter as the first filter in the list.
servletHandler.prependFilter(holder);
servletHandler.prependFilterMapping(mapping);
if (LOG.isDebugEnabled())
LOG.debug("Adding {} mapped to {} in {}", holder, pathSpec, servletContext);
return holder;
@ -137,7 +135,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
}
private final Configuration.ConfigurationCustomizer defaultCustomizer = new Configuration.ConfigurationCustomizer();
private WebSocketMapping mapping;
private WebSocketMappings mapping;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
@ -173,12 +171,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
@Override
public void init(FilterConfig config) throws ServletException
{
final ServletContext context = config.getServletContext();
String mappingKey = config.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM);
if (mappingKey == null)
throw new ServletException("the WebSocketMapping init param must be set");
mapping = WebSocketMapping.ensureMapping(context, mappingKey);
mapping = WebSocketMappings.ensureMapping(config.getServletContext());
String max = config.getInitParameter("idleTimeout");
if (max == null)

View File

@ -56,24 +56,14 @@ import org.slf4j.LoggerFactory;
* wrap that POJO with a {@link FrameHandler} and the customizer is used to configure the resulting
* {@link CoreSession}.</p>
*/
public class WebSocketMapping implements Dumpable, LifeCycle.Listener
public class WebSocketMappings implements Dumpable, LifeCycle.Listener
{
private static final Logger LOG = LoggerFactory.getLogger(WebSocketMapping.class);
private static final Logger LOG = LoggerFactory.getLogger(WebSocketMappings.class);
public static final String WEBSOCKET_MAPPING_ATTRIBUTE = WebSocketMappings.class.getName();
public static WebSocketMapping getMapping(ServletContext servletContext, String mappingKey)
public static WebSocketMappings getMapping(ServletContext servletContext)
{
Object mappingObject = servletContext.getAttribute(mappingKey);
if (mappingObject != null)
{
if (mappingObject instanceof WebSocketMapping)
return (WebSocketMapping)mappingObject;
else
throw new IllegalStateException(
String.format("ContextHandler attribute %s is not of type WebSocketMapping: {%s}",
mappingKey, mappingObject.toString()));
}
return null;
return (WebSocketMappings)servletContext.getAttribute(WEBSOCKET_MAPPING_ATTRIBUTE);
}
public WebSocketCreator getMapping(PathSpec pathSpec)
@ -82,13 +72,13 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
return cn == null ? null : cn.getWebSocketCreator();
}
public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey)
public static WebSocketMappings ensureMapping(ServletContext servletContext)
{
WebSocketMapping mapping = getMapping(servletContext, mappingKey);
WebSocketMappings mapping = getMapping(servletContext);
if (mapping == null)
{
mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(servletContext));
servletContext.setAttribute(mappingKey, mapping);
mapping = new WebSocketMappings(WebSocketServerComponents.getWebSocketComponents(servletContext));
servletContext.setAttribute(WEBSOCKET_MAPPING_ATTRIBUTE, mapping);
}
return mapping;
@ -133,18 +123,16 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]");
}
public static final String DEFAULT_KEY = "jetty.websocket.defaultMapping";
private final PathMappings<Negotiator> mappings = new PathMappings<>();
private final WebSocketComponents components;
private final Handshaker handshaker = Handshaker.newInstance();
public WebSocketMapping()
public WebSocketMappings()
{
this(new WebSocketComponents());
}
public WebSocketMapping(WebSocketComponents components)
public WebSocketMappings(WebSocketComponents components)
{
this.components = components;
}
@ -153,7 +141,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
public void lifeCycleStopping(LifeCycle context)
{
ContextHandler contextHandler = (ContextHandler)context;
WebSocketMapping mapping = contextHandler.getBean(WebSocketMapping.class);
WebSocketMappings mapping = contextHandler.getBean(WebSocketMappings.class);
if (mapping == this)
{
contextHandler.removeBean(mapping);