View Javadoc

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