View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2012 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;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Enumeration;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Queue;
28  import java.util.concurrent.ConcurrentLinkedQueue;
29  import javax.servlet.http.HttpServletRequest;
30  import javax.servlet.http.HttpServletResponse;
31  
32  import org.eclipse.jetty.http.HttpException;
33  import org.eclipse.jetty.http.HttpParser;
34  import org.eclipse.jetty.io.ConnectedEndPoint;
35  import org.eclipse.jetty.server.AbstractHttpConnection;
36  import org.eclipse.jetty.server.BlockingHttpConnection;
37  import org.eclipse.jetty.util.QuotedStringTokenizer;
38  import org.eclipse.jetty.util.component.AbstractLifeCycle;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  
42  /**
43   * Factory to create WebSocket connections
44   */
45  public class WebSocketFactory extends AbstractLifeCycle
46  {
47      private static final Logger LOG = Log.getLogger(WebSocketFactory.class);
48      private final Queue<WebSocketServletConnection> connections = new ConcurrentLinkedQueue<WebSocketServletConnection>();
49  
50      public interface Acceptor
51      {
52          /* ------------------------------------------------------------ */
53          /**
54           * <p>Factory method that applications needs to implement to return a
55           * {@link WebSocket} object.</p>
56           * @param request the incoming HTTP upgrade request
57           * @param protocol the websocket sub protocol
58           * @return a new {@link WebSocket} object that will handle websocket events.
59           */
60          WebSocket doWebSocketConnect(HttpServletRequest request, String protocol);
61  
62          /* ------------------------------------------------------------ */
63          /**
64           * <p>Checks the origin of an incoming WebSocket handshake request.</p>
65           * @param request the incoming HTTP upgrade request
66           * @param origin the origin URI
67           * @return boolean to indicate that the origin is acceptable.
68           */
69          boolean checkOrigin(HttpServletRequest request, String origin);
70      }
71  
72      private final Map<String,Class<? extends Extension>> _extensionClasses = new HashMap<String, Class<? extends Extension>>();
73      {
74          _extensionClasses.put("identity",IdentityExtension.class);
75          _extensionClasses.put("fragment",FragmentExtension.class);
76          _extensionClasses.put("x-deflate-frame",DeflateFrameExtension.class);
77      }
78  
79      private final Acceptor _acceptor;
80      private WebSocketBuffers _buffers;
81      private int _maxIdleTime = 300000;
82      private int _maxTextMessageSize = 16 * 1024;
83      private int _maxBinaryMessageSize = -1;
84  
85      public WebSocketFactory(Acceptor acceptor)
86      {
87          this(acceptor, 64 * 1024);
88      }
89  
90      public WebSocketFactory(Acceptor acceptor, int bufferSize)
91      {
92          _buffers = new WebSocketBuffers(bufferSize);
93          _acceptor = acceptor;
94      }
95  
96      /**
97       * @return A modifiable map of extension name to extension class
98       */
99      public Map<String,Class<? extends Extension>> getExtensionClassesMap()
100     {
101         return _extensionClasses;
102     }
103 
104     /**
105      * Get the maxIdleTime.
106      *
107      * @return the maxIdleTime
108      */
109     public long getMaxIdleTime()
110     {
111         return _maxIdleTime;
112     }
113 
114     /**
115      * Set the maxIdleTime.
116      *
117      * @param maxIdleTime the maxIdleTime to set
118      */
119     public void setMaxIdleTime(int maxIdleTime)
120     {
121         _maxIdleTime = maxIdleTime;
122     }
123 
124     /**
125      * Get the bufferSize.
126      *
127      * @return the bufferSize
128      */
129     public int getBufferSize()
130     {
131         return _buffers.getBufferSize();
132     }
133 
134     /**
135      * Set the bufferSize.
136      *
137      * @param bufferSize the bufferSize to set
138      */
139     public void setBufferSize(int bufferSize)
140     {
141         if (bufferSize != getBufferSize())
142             _buffers = new WebSocketBuffers(bufferSize);
143     }
144 
145     /**
146      * @return The initial maximum text message size (in characters) for a connection
147      */
148     public int getMaxTextMessageSize()
149     {
150         return _maxTextMessageSize;
151     }
152 
153     /**
154      * Set the initial maximum text message size for a connection. This can be changed by
155      * the application calling {@link WebSocket.Connection#setMaxTextMessageSize(int)}.
156      * @param maxTextMessageSize The default maximum text message size (in characters) for a connection
157      */
158     public void setMaxTextMessageSize(int maxTextMessageSize)
159     {
160         _maxTextMessageSize = maxTextMessageSize;
161     }
162 
163     /**
164      * @return The initial maximum binary message size (in bytes)  for a connection
165      */
166     public int getMaxBinaryMessageSize()
167     {
168         return _maxBinaryMessageSize;
169     }
170 
171     /**
172      * Set the initial maximum binary message size for a connection. This can be changed by
173      * the application calling {@link WebSocket.Connection#setMaxBinaryMessageSize(int)}.
174      * @param maxBinaryMessageSize The default maximum binary message size (in bytes) for a connection
175      */
176     public void setMaxBinaryMessageSize(int maxBinaryMessageSize)
177     {
178         _maxBinaryMessageSize = maxBinaryMessageSize;
179     }
180 
181     @Override
182     protected void doStop() throws Exception
183     {
184         closeConnections();
185     }
186 
187     /**
188      * Upgrade the request/response to a WebSocket Connection.
189      * <p>This method will not normally return, but will instead throw a
190      * UpgradeConnectionException, to exit HTTP handling and initiate
191      * WebSocket handling of the connection.
192      *
193      * @param request   The request to upgrade
194      * @param response  The response to upgrade
195      * @param websocket The websocket handler implementation to use
196      * @param protocol  The websocket protocol
197      * @throws IOException in case of I/O errors
198      */
199     public void upgrade(HttpServletRequest request, HttpServletResponse response, WebSocket websocket, String protocol)
200             throws IOException
201     {
202         if (!"websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
203             throw new IllegalStateException("!Upgrade:websocket");
204         if (!"HTTP/1.1".equals(request.getProtocol()))
205             throw new IllegalStateException("!HTTP/1.1");
206 
207         int draft = request.getIntHeader("Sec-WebSocket-Version");
208         if (draft < 0) {
209             // Old pre-RFC version specifications (header not present in RFC-6455)
210             draft = request.getIntHeader("Sec-WebSocket-Draft");
211         }
212         AbstractHttpConnection http = AbstractHttpConnection.getCurrentConnection();
213         if (http instanceof BlockingHttpConnection)
214             throw new IllegalStateException("Websockets not supported on blocking connectors");
215         ConnectedEndPoint endp = (ConnectedEndPoint)http.getEndPoint();
216 
217         List<String> extensions_requested = new ArrayList<String>();
218         @SuppressWarnings("unchecked")
219         Enumeration<String> e = request.getHeaders("Sec-WebSocket-Extensions");
220         while (e.hasMoreElements())
221         {
222             QuotedStringTokenizer tok = new QuotedStringTokenizer(e.nextElement(),",");
223             while (tok.hasMoreTokens())
224             {
225                 extensions_requested.add(tok.nextToken());
226             }
227         }
228 
229         final WebSocketServletConnection connection;
230         switch (draft)
231         {
232             case -1: // unspecified draft/version
233             case 0: // Old school draft/version
234             {
235                 connection = new WebSocketServletConnectionD00(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
236                 break;
237             }
238             case 1:
239             case 2:
240             case 3:
241             case 4:
242             case 5:
243             case 6:
244             {
245                 connection = new WebSocketServletConnectionD06(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
246                 break;
247             }
248             case 7:
249             case 8:
250             {
251                 List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionD08.OP_EXT_DATA, 16 - WebSocketConnectionD08.OP_EXT_CTRL, 3);
252                 connection = new WebSocketServletConnectionD08(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
253                 break;
254             }
255             case WebSocketConnectionRFC6455.VERSION: // RFC 6455 Version
256             {
257                 List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionRFC6455.OP_EXT_DATA, 16 - WebSocketConnectionRFC6455.OP_EXT_CTRL, 3);
258                 connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
259                 break;
260             }
261             default:
262             {
263                 LOG.warn("Unsupported Websocket version: " + draft);
264                 // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol
265                 // Using the examples as outlined
266                 response.setHeader("Sec-WebSocket-Version", "13, 8, 6, 0");
267                 throw new HttpException(400, "Unsupported websocket version specification: " + draft);
268             }
269         }
270 
271         addConnection(connection);
272 
273         // Set the defaults
274         connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize);
275         connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize);
276 
277         // Let the connection finish processing the handshake
278         connection.handshake(request, response, protocol);
279         response.flushBuffer();
280 
281         // Give the connection any unused data from the HTTP connection.
282         connection.fillBuffersFrom(((HttpParser)http.getParser()).getHeaderBuffer());
283         connection.fillBuffersFrom(((HttpParser)http.getParser()).getBodyBuffer());
284 
285         // Tell jetty about the new connection
286         LOG.debug("Websocket upgrade {} {} {} {}",request.getRequestURI(),draft,protocol,connection);
287         request.setAttribute("org.eclipse.jetty.io.Connection", connection);
288     }
289 
290     protected String[] parseProtocols(String protocol)
291     {
292         if (protocol == null)
293             return new String[]{null};
294         protocol = protocol.trim();
295         if (protocol == null || protocol.length() == 0)
296             return new String[]{null};
297         String[] passed = protocol.split("\\s*,\\s*");
298         String[] protocols = new String[passed.length + 1];
299         System.arraycopy(passed, 0, protocols, 0, passed.length);
300         return protocols;
301     }
302 
303     public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response)
304             throws IOException
305     {
306         if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
307         {
308             String origin = request.getHeader("Origin");
309             if (origin==null)
310                 origin = request.getHeader("Sec-WebSocket-Origin");
311             if (!_acceptor.checkOrigin(request,origin))
312             {
313                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
314                 return false;
315             }
316 
317             // Try each requested protocol
318             WebSocket websocket = null;
319 
320             @SuppressWarnings("unchecked")
321             Enumeration<String> protocols = request.getHeaders("Sec-WebSocket-Protocol");
322             String protocol=null;
323             while (protocol==null && protocols!=null && protocols.hasMoreElements())
324             {
325                 String candidate = protocols.nextElement();
326                 for (String p : parseProtocols(candidate))
327                 {
328                     websocket = _acceptor.doWebSocketConnect(request, p);
329                     if (websocket != null)
330                     {
331                         protocol = p;
332                         break;
333                     }
334                 }
335             }
336 
337             // Did we get a websocket?
338             if (websocket == null)
339             {
340                 // Try with no protocol
341                 websocket = _acceptor.doWebSocketConnect(request, null);
342 
343                 if (websocket==null)
344                 {
345                     response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
346                     return false;
347                 }
348             }
349 
350             // Send the upgrade
351             upgrade(request, response, websocket, protocol);
352             return true;
353         }
354 
355         return false;
356     }
357 
358     public List<Extension> initExtensions(List<String> requested,int maxDataOpcodes,int maxControlOpcodes,int maxReservedBits)
359     {
360         List<Extension> extensions = new ArrayList<Extension>();
361         for (String rExt : requested)
362         {
363             QuotedStringTokenizer tok = new QuotedStringTokenizer(rExt,";");
364             String extName=tok.nextToken().trim();
365             Map<String,String> parameters = new HashMap<String,String>();
366             while (tok.hasMoreTokens())
367             {
368                 QuotedStringTokenizer nv = new QuotedStringTokenizer(tok.nextToken().trim(),"=");
369                 String name=nv.nextToken().trim();
370                 String value=nv.hasMoreTokens()?nv.nextToken().trim():null;
371                 parameters.put(name,value);
372             }
373 
374             Extension extension = newExtension(extName);
375 
376             if (extension==null)
377                 continue;
378 
379             if (extension.init(parameters))
380             {
381                 LOG.debug("add {} {}",extName,parameters);
382                 extensions.add(extension);
383             }
384         }
385         LOG.debug("extensions={}",extensions);
386         return extensions;
387     }
388 
389     private Extension newExtension(String name)
390     {
391         try
392         {
393             Class<? extends Extension> extClass = _extensionClasses.get(name);
394             if (extClass!=null)
395                 return extClass.newInstance();
396         }
397         catch (Exception e)
398         {
399             LOG.warn(e);
400         }
401 
402         return null;
403     }
404 
405     protected boolean addConnection(WebSocketServletConnection connection)
406     {
407         return isRunning() && connections.add(connection);
408     }
409 
410     protected boolean removeConnection(WebSocketServletConnection connection)
411     {
412         return connections.remove(connection);
413     }
414 
415     protected void closeConnections()
416     {
417         for (WebSocketServletConnection connection : connections)
418             connection.shutdown();
419     }
420 }