View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.websocket.server;
20  
21  import java.io.IOException;
22  import java.util.EnumSet;
23  
24  import javax.servlet.DispatcherType;
25  import javax.servlet.Filter;
26  import javax.servlet.FilterChain;
27  import javax.servlet.FilterConfig;
28  import javax.servlet.FilterRegistration;
29  import javax.servlet.ServletContext;
30  import javax.servlet.ServletException;
31  import javax.servlet.ServletRequest;
32  import javax.servlet.ServletResponse;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  
36  import org.eclipse.jetty.http.pathmap.MappedResource;
37  import org.eclipse.jetty.http.pathmap.PathMappings;
38  import org.eclipse.jetty.http.pathmap.PathSpec;
39  import org.eclipse.jetty.io.ByteBufferPool;
40  import org.eclipse.jetty.io.MappedByteBufferPool;
41  import org.eclipse.jetty.servlet.FilterHolder;
42  import org.eclipse.jetty.servlet.ServletContextHandler;
43  import org.eclipse.jetty.util.annotation.ManagedAttribute;
44  import org.eclipse.jetty.util.annotation.ManagedObject;
45  import org.eclipse.jetty.util.component.ContainerLifeCycle;
46  import org.eclipse.jetty.util.component.Dumpable;
47  import org.eclipse.jetty.util.log.Log;
48  import org.eclipse.jetty.util.log.Logger;
49  import org.eclipse.jetty.websocket.api.WebSocketPolicy;
50  import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
51  
52  /**
53   * Inline Servlet Filter to capture WebSocket upgrade requests and perform path mappings to {@link WebSocketCreator} objects.
54   */
55  @ManagedObject("WebSocket Upgrade Filter")
56  public class WebSocketUpgradeFilter extends ContainerLifeCycle implements Filter, MappedWebSocketCreator, Dumpable
57  {
58      public static final String CONTEXT_ATTRIBUTE_KEY = "contextAttributeKey";
59      private static final Logger LOG = Log.getLogger(WebSocketUpgradeFilter.class);
60  
61      public static WebSocketUpgradeFilter configureContext(ServletContextHandler context) throws ServletException
62      {
63          // Prevent double configure
64          WebSocketUpgradeFilter filter = (WebSocketUpgradeFilter)context.getAttribute(WebSocketUpgradeFilter.class.getName());
65          if (filter != null)
66          {
67              return filter;
68          }
69          
70          // Dynamically add filter
71          filter = new WebSocketUpgradeFilter();
72          filter.setToAttribute(context, WebSocketUpgradeFilter.class.getName());
73  
74          String name = "Jetty_WebSocketUpgradeFilter";
75          String pathSpec = "/*";
76          EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
77  
78          FilterHolder fholder = new FilterHolder(filter);
79          fholder.setName(name);
80          fholder.setAsyncSupported(true);
81          fholder.setInitParameter(CONTEXT_ATTRIBUTE_KEY,WebSocketUpgradeFilter.class.getName());
82          context.addFilter(fholder,pathSpec,dispatcherTypes);
83  
84          if (LOG.isDebugEnabled())
85          {
86              LOG.debug("Adding [{}] {} mapped to {} to {}",name,filter,pathSpec,context);
87          }
88  
89          return filter;
90      }
91  
92      public static WebSocketUpgradeFilter configureContext(ServletContext context) throws ServletException
93      {
94          // Prevent double configure
95          WebSocketUpgradeFilter filter = (WebSocketUpgradeFilter)context.getAttribute(WebSocketUpgradeFilter.class.getName());
96          if (filter != null)
97          {
98              return filter;
99          }
100         
101         // Dynamically add filter
102         filter = new WebSocketUpgradeFilter();
103         filter.setToAttribute(context, WebSocketUpgradeFilter.class.getName());
104 
105         String name = "Jetty_Dynamic_WebSocketUpgradeFilter";
106         String pathSpec = "/*";
107         EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
108         boolean isMatchAfter = false;
109         String urlPatterns[] = { pathSpec };
110 
111         FilterRegistration.Dynamic dyn = context.addFilter(name,filter);
112         dyn.setAsyncSupported(true);
113         dyn.setInitParameter(CONTEXT_ATTRIBUTE_KEY,WebSocketUpgradeFilter.class.getName());
114         dyn.addMappingForUrlPatterns(dispatcherTypes,isMatchAfter,urlPatterns);
115 
116         if (LOG.isDebugEnabled())
117         {
118             LOG.debug("Adding [{}] {} mapped to {} to {}",name,filter,pathSpec,context);
119         }
120 
121         return filter;
122     }
123 
124     private final WebSocketServerFactory factory;
125     private final PathMappings<WebSocketCreator> pathmap = new PathMappings<>();
126     private String fname;
127     private boolean alreadySetToAttribute = false;
128 
129     public WebSocketUpgradeFilter()
130     {
131         this(WebSocketPolicy.newServerPolicy(),new MappedByteBufferPool());
132     }
133 
134     public WebSocketUpgradeFilter(WebSocketPolicy policy, ByteBufferPool bufferPool)
135     {
136         factory = new WebSocketServerFactory(policy,bufferPool);
137         addBean(factory,true);
138     }
139 
140     @Override
141     public void addMapping(PathSpec spec, WebSocketCreator creator)
142     {
143         pathmap.put(spec,creator);
144     }
145 
146     @Override
147     public void destroy()
148     {
149         factory.cleanup();
150         pathmap.reset();
151         super.destroy();
152     }
153 
154     @Override
155     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
156     {
157         if (factory == null)
158         {
159             // no factory, cannot operate
160             LOG.debug("WebSocketUpgradeFilter is not operational - no WebSocketServletFactory configured");
161             chain.doFilter(request,response);
162             return;
163         }
164 
165         if (LOG.isDebugEnabled())
166         {
167             LOG.debug(".doFilter({}) - {}",fname,chain);
168         }
169 
170         if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse))
171         {
172             HttpServletRequest httpreq = (HttpServletRequest)request;
173             HttpServletResponse httpresp = (HttpServletResponse)response;
174 
175             // Since this is a filter, we need to be smart about determining the target path
176             String contextPath = httpreq.getContextPath();
177             String target = httpreq.getRequestURI();
178             if (target.startsWith(contextPath))
179             {
180                 target = target.substring(contextPath.length());
181             }
182 
183             if (factory.isUpgradeRequest(httpreq,httpresp))
184             {
185                 LOG.debug("target = [{}]",target);
186 
187                 MappedResource<WebSocketCreator> resource = pathmap.getMatch(target);
188                 if (resource == null)
189                 {
190                     if (LOG.isDebugEnabled())
191                     {
192                         LOG.debug("WebSocket Upgrade on {} has no associated endpoint",target);
193                         LOG.debug("PathMappings: {}",pathmap.dump());
194                     }
195                     // no match.
196                     chain.doFilter(request,response);
197                     return;
198                 }
199                 LOG.debug("WebSocket Upgrade detected on {} for endpoint {}",target,resource);
200 
201                 WebSocketCreator creator = resource.getResource();
202 
203                 // Store PathSpec resource mapping as request attribute
204                 httpreq.setAttribute(PathSpec.class.getName(),resource.getPathSpec());
205 
206                 // We have an upgrade request
207                 if (factory.acceptWebSocket(creator,httpreq,httpresp))
208                 {
209                     // We have a socket instance created
210                     return;
211                 }
212 
213                 // If we reach this point, it means we had an incoming request to upgrade
214                 // but it was either not a proper websocket upgrade, or it was possibly rejected
215                 // due to incoming request constraints (controlled by WebSocketCreator)
216                 if (response.isCommitted())
217                 {
218                     // not much we can do at this point.
219                     return;
220                 }
221             }
222         }
223 
224         // not an Upgrade request
225         chain.doFilter(request,response);
226     }
227 
228     @Override
229     public String dump()
230     {
231         return ContainerLifeCycle.dump(this);
232     }
233 
234     @Override
235     public void dump(Appendable out, String indent) throws IOException
236     {
237         out.append(indent).append(" +- pathmap=").append(pathmap.toString()).append("\n");
238         pathmap.dump(out,indent + "   ");
239     }
240 
241     public WebSocketServerFactory getFactory()
242     {
243         return factory;
244     }
245 
246     @ManagedAttribute(value = "mappings", readonly = true)
247     @Override
248     public PathMappings<WebSocketCreator> getMappings()
249     {
250         return pathmap;
251     }
252 
253     @Override
254     public void init(FilterConfig config) throws ServletException
255     {
256         fname = config.getFilterName();
257 
258         try
259         {
260             ServletContext ctx = config.getServletContext();
261             factory.init(ctx);
262             WebSocketPolicy policy = factory.getPolicy();
263 
264             String max = config.getInitParameter("maxIdleTime");
265             if (max != null)
266             {
267                 policy.setIdleTimeout(Long.parseLong(max));
268             }
269 
270             max = config.getInitParameter("maxTextMessageSize");
271             if (max != null)
272             {
273                 policy.setMaxTextMessageSize(Integer.parseInt(max));
274             }
275 
276             max = config.getInitParameter("maxBinaryMessageSize");
277             if (max != null)
278             {
279                 policy.setMaxBinaryMessageSize(Integer.parseInt(max));
280             }
281 
282             max = config.getInitParameter("inputBufferSize");
283             if (max != null)
284             {
285                 policy.setInputBufferSize(Integer.parseInt(max));
286             }
287 
288             String key = config.getInitParameter(CONTEXT_ATTRIBUTE_KEY);
289             if (key == null)
290             {
291                 // assume default
292                 key = WebSocketUpgradeFilter.class.getName();
293             }
294             
295             setToAttribute(ctx, key);
296             
297             factory.start();
298         }
299         catch (Exception x)
300         {
301             throw new ServletException(x);
302         }
303     }
304     
305     private void setToAttribute(ServletContextHandler context, String key) throws ServletException
306     {
307         if(alreadySetToAttribute)
308         {
309             return;
310         }
311         
312         if (context.getAttribute(key) != null)
313         {
314             throw new ServletException(WebSocketUpgradeFilter.class.getName() + 
315                     " is defined twice for the same context attribute key '" + key
316                     + "'.  Make sure you have different init-param '" + 
317                     CONTEXT_ATTRIBUTE_KEY + "' values set");
318         }
319         context.setAttribute(key,this);
320 
321         alreadySetToAttribute = true;
322     }
323 
324     public void setToAttribute(ServletContext context, String key) throws ServletException
325     {
326         if(alreadySetToAttribute)
327         {
328             return;
329         }
330         
331         if (context.getAttribute(key) != null)
332         {
333             throw new ServletException(WebSocketUpgradeFilter.class.getName() + 
334                     " is defined twice for the same context attribute key '" + key
335                     + "'.  Make sure you have different init-param '" + 
336                     CONTEXT_ATTRIBUTE_KEY + "' values set");
337         }
338         context.setAttribute(key,this);
339 
340         alreadySetToAttribute = true;
341     }
342 
343     @Override
344     public String toString()
345     {
346         return String.format("%s[factory=%s,pathmap=%s]",this.getClass().getSimpleName(),factory,pathmap);
347     }
348 }