View Javadoc

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