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