Issue #3446 - allow jetty-websocket upgrades with WebSocketUpgradeFilter

JettyWebSocketServerContainer moved out of internal and is now used
in a similar way to the Javax server container, and can now be
used to configure upgrades using the shared mapping with
the WSUpgradeFilter

the JettyWSSCI now works in the same way as the JavaxWSSCI, the
JettyWebSocketEmbeddedStarter has been removed and the configureContext
now returns the JettyWebSocketServerContainer

WebSocket upgrade failures due to exceptions will now be logged as
a warning in WebSocketMapping rather than being ignored

Signed-off-by: lachan-roberts <lachlan@webtide.com>
This commit is contained in:
lachan-roberts 2019-03-11 01:53:44 +11:00
parent 265e0be6f7
commit e9df97e314
5 changed files with 173 additions and 150 deletions

View File

@ -19,8 +19,8 @@
package org.eclipse.jetty.websocket.server;
import java.util.concurrent.CompletableFuture;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
@ -29,7 +29,6 @@ import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandlerFactory;
import org.eclipse.jetty.websocket.common.WebSocketContainer;
import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.server.internal.DelegatedJettyServletUpgradeRequest;
import org.eclipse.jetty.websocket.server.internal.JettyWebSocketServerContainer;
import org.eclipse.jetty.websocket.server.internal.UpgradeResponseAdapter;
import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
@ -40,14 +39,13 @@ public class JettyServerFrameHandlerFactory
implements FrameHandlerFactory, LifeCycle.Listener
{
public static JettyServerFrameHandlerFactory ensureFactory(ServletContext servletContext)
throws ServletException
{
ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "Jetty Websocket");
JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class);
if (factory == null)
{
JettyWebSocketServerContainer container = new JettyWebSocketServerContainer(contextHandler);
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(servletContext);
servletContext.setAttribute(WebSocketContainer.class.getName(), container);
factory = new JettyServerFrameHandlerFactory(container);
contextHandler.addManaged(factory);

View File

@ -16,7 +16,7 @@
// ========================================================================
//
package org.eclipse.jetty.websocket.server.internal;
package org.eclipse.jetty.websocket.server;
import java.util.ArrayList;
import java.util.Collection;
@ -24,37 +24,93 @@ import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import javax.servlet.ServletContext;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.common.SessionTracker;
import org.eclipse.jetty.websocket.common.WebSocketContainer;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.WebSocketException;
import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketMapping;
public class JettyWebSocketServerContainer implements WebSocketContainer
public class JettyWebSocketServerContainer extends ContainerLifeCycle implements WebSocketContainer, LifeCycle.Listener
{
public static JettyWebSocketServerContainer ensureContainer(ServletContext servletContext)
{
ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "Javax Websocket");
if (contextHandler.getServer() == null)
throw new IllegalStateException("Server has not been set on the ServletContextHandler");
JettyWebSocketServerContainer container = contextHandler.getBean(JettyWebSocketServerContainer.class);
if (container==null)
{
// Find Pre-Existing executor
Executor executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor");
if (executor == null)
executor = contextHandler.getServer().getThreadPool();
// Create the Jetty ServerContainer implementation
container = new JettyWebSocketServerContainer(
WebSocketMapping.ensureMapping(servletContext, WebSocketMapping.DEFAULT_KEY),
WebSocketComponents.ensureWebSocketComponents(servletContext), executor);
contextHandler.addManaged(container);
contextHandler.addLifeCycleListener(container);
}
return container;
}
private final static Logger LOG = Log.getLogger(JettyWebSocketServerContainer.class);
private final WebSocketMapping webSocketMapping;
private final WebSocketComponents webSocketComponents;
private final FrameHandlerFactory frameHandlerFactory;
private final Executor executor;
private final FrameHandler.ConfigurationCustomizer customizer = new FrameHandler.ConfigurationCustomizer();
private final List<WebSocketSessionListener> sessionListeners = new ArrayList<>();
private final SessionTracker sessionTracker = new SessionTracker();
public JettyWebSocketServerContainer(ContextHandler handler)
/**
* Main entry point for {@link JettyWebSocketServletContainerInitializer}.
* @param webSocketMapping the {@link WebSocketMapping} that this container belongs to
* @param webSocketComponents the {@link WebSocketComponents} instance to use
* @param executor the {@link Executor} to use
*/
public JettyWebSocketServerContainer(WebSocketMapping webSocketMapping, WebSocketComponents webSocketComponents, Executor executor)
{
Executor executor = (Executor) handler
.getAttribute("org.eclipse.jetty.server.Executor");
if (executor == null)
{
executor = handler.getServer().getThreadPool();
}
if (executor == null)
{
executor = new QueuedThreadPool(); // default settings
}
this.webSocketMapping = webSocketMapping;
this.webSocketComponents = webSocketComponents;
this.executor = executor;
this.frameHandlerFactory = new JettyServerFrameHandlerFactory(this);
addSessionListener(sessionTracker);
handler.addBean(sessionTracker);
}
public void addMapping(String pathSpec, WebSocketCreator creator)
{
addMapping(WebSocketMapping.parsePathSpec(pathSpec), creator);
}
public void addMapping(PathSpec pathSpec, WebSocketCreator creator) throws WebSocketException
{
if (webSocketMapping.getMapping(pathSpec) != null)
throw new WebSocketException("Duplicate WebSocket Mapping for PathSpec");
webSocketMapping.addMapping(pathSpec, creator, frameHandlerFactory, customizer);
}
@Override

View File

@ -18,7 +18,6 @@
package org.eclipse.jetty.websocket.server;
import java.util.Collections;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
@ -27,7 +26,6 @@ import javax.servlet.ServletException;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
@ -41,47 +39,24 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain
{
private static final Logger LOG = Log.getLogger(JettyWebSocketServletContainerInitializer.class);
public static class JettyWebSocketEmbeddedStarter extends AbstractLifeCycle implements ServletContextHandler.ServletContainerInitializerCaller
public static JettyWebSocketServerContainer configureContext(ServletContextHandler context)
{
private ServletContainerInitializer sci;
private ServletContextHandler context;
WebSocketComponents components = WebSocketComponents.ensureWebSocketComponents(context.getServletContext());
FilterHolder filterHolder = WebSocketUpgradeFilter.ensureFilter(context.getServletContext());
WebSocketMapping mapping = WebSocketMapping.ensureMapping(context.getServletContext(), WebSocketMapping.DEFAULT_KEY);
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(context.getServletContext());
JettyServerFrameHandlerFactory.ensureFactory(context.getServletContext());
public JettyWebSocketEmbeddedStarter(ServletContainerInitializer sci, ServletContextHandler context)
{
this.sci = sci;
this.context = context;
}
if (LOG.isDebugEnabled())
LOG.debug("onStartup {} {} {} {}", container, mapping, filterHolder, components);
public void doStart()
{
try
{
Set<Class<?>> c = Collections.emptySet();
sci.onStartup(c, context.getServletContext());
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
public static void configureContext(ServletContextHandler context)
{
JettyWebSocketServletContainerInitializer sci = new JettyWebSocketServletContainerInitializer();
JettyWebSocketEmbeddedStarter starter = new JettyWebSocketEmbeddedStarter(sci, context);
context.addBean(starter);
return container;
}
@Override
public void onStartup(Set<Class<?>> c, ServletContext servletContext) throws ServletException
public void onStartup(Set<Class<?>> c, ServletContext context) throws ServletException
{
WebSocketComponents components = WebSocketComponents.ensureWebSocketComponents(servletContext);
FilterHolder filterHolder = WebSocketUpgradeFilter.ensureFilter(servletContext);
WebSocketMapping mapping = WebSocketMapping.ensureMapping(servletContext, WebSocketMapping.DEFAULT_KEY);
JettyServerFrameHandlerFactory factory = JettyServerFrameHandlerFactory.ensureFactory(servletContext);
if (LOG.isDebugEnabled())
LOG.debug("onStartup {} {} {} {}", factory, mapping, filterHolder, components);
ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context,"Jetty WebSocket SCI");
JettyWebSocketServletContainerInitializer.configureContext(contextHandler);
}
}

View File

@ -93,96 +93,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
return mapping;
}
public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.servlet.WebSocketMapping";
private final PathMappings<Negotiator> mappings = new PathMappings<>();
private final WebSocketComponents components;
private final Handshaker handshaker = Handshaker.newInstance();
public WebSocketMapping()
{
this(new WebSocketComponents());
}
public WebSocketMapping(WebSocketComponents components)
{
this.components = components;
}
@Override
public void lifeCycleStopping(LifeCycle context)
{
ContextHandler contextHandler = (ContextHandler) context;
WebSocketMapping mapping = contextHandler.getBean(WebSocketMapping.class);
if (mapping == this)
{
contextHandler.removeBean(mapping);
mappings.reset();
}
}
/**
* Manually add a WebSocket mapping.
* <p>
* If mapping is added before this configuration is started, then it is persisted through
* stop/start of this configuration's lifecycle. Otherwise it will be removed when
* this configuration is stopped.
* </p>
*
* @param pathSpec the pathspec to respond on
* @param creator the websocket creator to activate on the provided mapping.
* @param factory the factory to use to create a FrameHandler for the websocket
* @param customizer the customizer to use to customize the WebSocket session.
*/
public void addMapping(PathSpec pathSpec, WebSocketCreator creator, FrameHandlerFactory factory, FrameHandler.Customizer customizer)
throws WebSocketException
{
// TODO evaluate why this can't be done
//if (getMapping(pathSpec) != null)
// throw new WebSocketException("Duplicate WebSocket Mapping for PathSpec");
mappings.put(pathSpec, new Negotiator(creator, factory, customizer));
}
public WebSocketCreator getMapping(PathSpec pathSpec)
{
Negotiator cn = mappings.get(pathSpec);
return cn == null?null:cn.getWebSocketCreator();
}
public boolean removeMapping(PathSpec pathSpec)
{
return mappings.remove(pathSpec);
}
@Override
public String dump()
{
return Dumpable.dump(this);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
Dumpable.dumpObjects(out, indent, this, mappings);
}
/**
* Get the matching {@link MappedResource} for the provided target.
*
* @param target the target path
* @return the matching resource, or null if no match.
*/
public WebSocketNegotiator getMatchedNegotiator(String target, Consumer<PathSpec> pathSpecConsumer)
{
MappedResource<Negotiator> mapping = this.mappings.getMatch(target);
if (mapping == null)
return null;
pathSpecConsumer.accept(mapping.getPathSpec());
return mapping.getResource();
}
/**
* Parse a PathSpec string into a PathSpec instance.
* <p>
@ -222,6 +132,91 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]");
}
public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.servlet.WebSocketMapping";
private final PathMappings<Negotiator> mappings = new PathMappings<>();
private final WebSocketComponents components;
private final Handshaker handshaker = Handshaker.newInstance();
public WebSocketMapping()
{
this(new WebSocketComponents());
}
public WebSocketMapping(WebSocketComponents components)
{
this.components = components;
}
@Override
public void lifeCycleStopping(LifeCycle context)
{
ContextHandler contextHandler = (ContextHandler) context;
WebSocketMapping mapping = contextHandler.getBean(WebSocketMapping.class);
if (mapping == this)
{
contextHandler.removeBean(mapping);
mappings.reset();
}
}
@Override
public String dump()
{
return Dumpable.dump(this);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
Dumpable.dumpObjects(out, indent, this, mappings);
}
/**
* Manually add a WebSocket mapping.
* <p>
* If mapping is added before this configuration is started, then it is persisted through
* stop/start of this configuration's lifecycle. Otherwise it will be removed when
* this configuration is stopped.
* </p>
*
* @param pathSpec the pathspec to respond on
* @param creator the websocket creator to activate on the provided mapping.
* @param factory the factory to use to create a FrameHandler for the websocket
* @param customizer the customizer to use to customize the WebSocket session.
*/
public void addMapping(PathSpec pathSpec, WebSocketCreator creator, FrameHandlerFactory factory, FrameHandler.Customizer customizer) throws WebSocketException
{
mappings.put(pathSpec, new Negotiator(creator, factory, customizer));
}
public WebSocketCreator getMapping(PathSpec pathSpec)
{
Negotiator cn = mappings.get(pathSpec);
return cn == null?null:cn.getWebSocketCreator();
}
public boolean removeMapping(PathSpec pathSpec)
{
return mappings.remove(pathSpec);
}
/**
* Get the matching {@link MappedResource} for the provided target.
*
* @param target the target path
* @return the matching resource, or null if no match.
*/
public WebSocketNegotiator getMatchedNegotiator(String target, Consumer<PathSpec> pathSpecConsumer)
{
MappedResource<Negotiator> mapping = this.mappings.getMatch(target);
if (mapping == null)
return null;
pathSpecConsumer.accept(mapping.getPathSpec());
return mapping.getResource();
}
public boolean upgrade(HttpServletRequest request, HttpServletResponse response, FrameHandler.Customizer defaultCustomizer)
{
try
@ -251,9 +246,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
}
catch (Exception e)
{
if (LOG.isDebugEnabled())
LOG.debug("Unable to upgrade: "+e);
LOG.ignore(e);
LOG.warn("Error during upgrade: ", e);
}
return false;
}

View File

@ -189,9 +189,10 @@ public abstract class WebSocketServlet extends HttpServlet
@Override
public void addMapping(PathSpec pathSpec, WebSocketCreator creator)
{
// TODO a bit fragile. This code knows that only the JettyFHF is added directly as a been
ServletContext servletContext = getServletContext();
ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "WebSocketServlet");
// TODO: a bit fragile, this code knows that only the JettyFHF is added as a bean
FrameHandlerFactory frameHandlerFactory = contextHandler.getBean(FrameHandlerFactory.class);
if (frameHandlerFactory==null)