blob: a189155d486578a862c6bf5cf61961b4e949fa49 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2016 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.server;
import java.io.IOException;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
/**
* Inline Servlet Filter to capture WebSocket upgrade requests and perform path mappings to {@link WebSocketCreator} objects.
*/
@ManagedObject("WebSocket Upgrade Filter")
public class WebSocketUpgradeFilter extends ContainerLifeCycle implements Filter, MappedWebSocketCreator, Dumpable
{
public static final String CONTEXT_ATTRIBUTE_KEY = "contextAttributeKey";
private static final Logger LOG = Log.getLogger(WebSocketUpgradeFilter.class);
public static WebSocketUpgradeFilter configureContext(ServletContextHandler context) throws ServletException
{
// Prevent double configure
WebSocketUpgradeFilter filter = (WebSocketUpgradeFilter)context.getAttribute(WebSocketUpgradeFilter.class.getName());
if (filter != null)
{
return filter;
}
// Dynamically add filter
filter = new WebSocketUpgradeFilter();
filter.setToAttribute(context, WebSocketUpgradeFilter.class.getName());
String name = "Jetty_WebSocketUpgradeFilter";
String pathSpec = "/*";
EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
FilterHolder fholder = new FilterHolder(filter);
fholder.setName(name);
fholder.setAsyncSupported(true);
fholder.setInitParameter(CONTEXT_ATTRIBUTE_KEY,WebSocketUpgradeFilter.class.getName());
context.addFilter(fholder,pathSpec,dispatcherTypes);
if (LOG.isDebugEnabled())
{
LOG.debug("Adding [{}] {} mapped to {} to {}",name,filter,pathSpec,context);
}
return filter;
}
public static WebSocketUpgradeFilter configureContext(ServletContext context) throws ServletException
{
// Prevent double configure
WebSocketUpgradeFilter filter = (WebSocketUpgradeFilter)context.getAttribute(WebSocketUpgradeFilter.class.getName());
if (filter != null)
{
return filter;
}
// Dynamically add filter
filter = new WebSocketUpgradeFilter();
filter.setToAttribute(context, WebSocketUpgradeFilter.class.getName());
String name = "Jetty_Dynamic_WebSocketUpgradeFilter";
String pathSpec = "/*";
EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
boolean isMatchAfter = false;
String urlPatterns[] = { pathSpec };
FilterRegistration.Dynamic dyn = context.addFilter(name,filter);
dyn.setAsyncSupported(true);
dyn.setInitParameter(CONTEXT_ATTRIBUTE_KEY,WebSocketUpgradeFilter.class.getName());
dyn.addMappingForUrlPatterns(dispatcherTypes,isMatchAfter,urlPatterns);
if (LOG.isDebugEnabled())
{
LOG.debug("Adding [{}] {} mapped to {} to {}",name,filter,pathSpec,context);
}
return filter;
}
private final WebSocketServerFactory factory;
private final PathMappings<WebSocketCreator> pathmap = new PathMappings<>();
private String fname;
private boolean alreadySetToAttribute = false;
public WebSocketUpgradeFilter()
{
this(WebSocketPolicy.newServerPolicy(),new MappedByteBufferPool());
}
public WebSocketUpgradeFilter(WebSocketPolicy policy, ByteBufferPool bufferPool)
{
factory = new WebSocketServerFactory(policy,bufferPool);
addBean(factory,true);
}
@Override
public void addMapping(PathSpec spec, WebSocketCreator creator)
{
pathmap.put(spec,creator);
}
@Override
public void destroy()
{
factory.cleanup();
pathmap.reset();
super.destroy();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
if (factory == null)
{
// no factory, cannot operate
LOG.debug("WebSocketUpgradeFilter is not operational - no WebSocketServletFactory configured");
chain.doFilter(request,response);
return;
}
if (LOG.isDebugEnabled())
{
LOG.debug(".doFilter({}) - {}",fname,chain);
}
if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse))
{
HttpServletRequest httpreq = (HttpServletRequest)request;
HttpServletResponse httpresp = (HttpServletResponse)response;
// Since this is a filter, we need to be smart about determining the target path
String contextPath = httpreq.getContextPath();
String target = httpreq.getRequestURI();
if (target.startsWith(contextPath))
{
target = target.substring(contextPath.length());
}
if (factory.isUpgradeRequest(httpreq,httpresp))
{
LOG.debug("target = [{}]",target);
MappedResource<WebSocketCreator> resource = pathmap.getMatch(target);
if (resource == null)
{
if (LOG.isDebugEnabled())
{
LOG.debug("WebSocket Upgrade on {} has no associated endpoint",target);
LOG.debug("PathMappings: {}",pathmap.dump());
}
// no match.
chain.doFilter(request,response);
return;
}
LOG.debug("WebSocket Upgrade detected on {} for endpoint {}",target,resource);
WebSocketCreator creator = resource.getResource();
// Store PathSpec resource mapping as request attribute
httpreq.setAttribute(PathSpec.class.getName(),resource.getPathSpec());
// We have an upgrade request
if (factory.acceptWebSocket(creator,httpreq,httpresp))
{
// We have a socket instance created
return;
}
// If we reach this point, it means we had an incoming request to upgrade
// but it was either not a proper websocket upgrade, or it was possibly rejected
// due to incoming request constraints (controlled by WebSocketCreator)
if (response.isCommitted())
{
// not much we can do at this point.
return;
}
}
}
// not an Upgrade request
chain.doFilter(request,response);
}
@Override
public String dump()
{
return ContainerLifeCycle.dump(this);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
out.append(indent).append(" +- pathmap=").append(pathmap.toString()).append("\n");
pathmap.dump(out,indent + " ");
}
public WebSocketServerFactory getFactory()
{
return factory;
}
@ManagedAttribute(value = "mappings", readonly = true)
@Override
public PathMappings<WebSocketCreator> getMappings()
{
return pathmap;
}
@Override
public void init(FilterConfig config) throws ServletException
{
fname = config.getFilterName();
try
{
ServletContext ctx = config.getServletContext();
factory.init(ctx);
WebSocketPolicy policy = factory.getPolicy();
String max = config.getInitParameter("maxIdleTime");
if (max != null)
{
policy.setIdleTimeout(Long.parseLong(max));
}
max = config.getInitParameter("maxTextMessageSize");
if (max != null)
{
policy.setMaxTextMessageSize(Integer.parseInt(max));
}
max = config.getInitParameter("maxBinaryMessageSize");
if (max != null)
{
policy.setMaxBinaryMessageSize(Integer.parseInt(max));
}
max = config.getInitParameter("inputBufferSize");
if (max != null)
{
policy.setInputBufferSize(Integer.parseInt(max));
}
String key = config.getInitParameter(CONTEXT_ATTRIBUTE_KEY);
if (key == null)
{
// assume default
key = WebSocketUpgradeFilter.class.getName();
}
setToAttribute(ctx, key);
factory.start();
}
catch (Exception x)
{
throw new ServletException(x);
}
}
private void setToAttribute(ServletContextHandler context, String key) throws ServletException
{
if(alreadySetToAttribute)
{
return;
}
if (context.getAttribute(key) != null)
{
throw new ServletException(WebSocketUpgradeFilter.class.getName() +
" is defined twice for the same context attribute key '" + key
+ "'. Make sure you have different init-param '" +
CONTEXT_ATTRIBUTE_KEY + "' values set");
}
context.setAttribute(key,this);
alreadySetToAttribute = true;
}
public void setToAttribute(ServletContext context, String key) throws ServletException
{
if(alreadySetToAttribute)
{
return;
}
if (context.getAttribute(key) != null)
{
throw new ServletException(WebSocketUpgradeFilter.class.getName() +
" is defined twice for the same context attribute key '" + key
+ "'. Make sure you have different init-param '" +
CONTEXT_ATTRIBUTE_KEY + "' values set");
}
context.setAttribute(key,this);
alreadySetToAttribute = true;
}
@Override
public String toString()
{
return String.format("%s[factory=%s,pathmap=%s]",this.getClass().getSimpleName(),factory,pathmap);
}
}