View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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      private int _minVersion;
85  
86      public WebSocketFactory(Acceptor acceptor)
87      {
88          this(acceptor, 64 * 1024, WebSocketConnectionRFC6455.VERSION);
89      }
90  
91      public WebSocketFactory(Acceptor acceptor, int bufferSize)
92      {
93          this(acceptor, bufferSize, WebSocketConnectionRFC6455.VERSION);
94      }
95  
96      public WebSocketFactory(Acceptor acceptor, int bufferSize, int minVersion)
97      {
98          _buffers = new WebSocketBuffers(bufferSize);
99          _acceptor = acceptor;
100         _minVersion=WebSocketConnectionRFC6455.VERSION;
101     }
102 
103     public int getMinVersion()
104     {
105         return _minVersion;
106     }
107 
108     /* ------------------------------------------------------------ */
109     /**
110      * @param minVersion The minimum support version (default RCF6455.VERSION == 13 )
111      */
112     public void setMinVersion(int minVersion)
113     {
114         _minVersion = minVersion;
115     }
116 
117     /**
118      * @return A modifiable map of extension name to extension class
119      */
120     public Map<String,Class<? extends Extension>> getExtensionClassesMap()
121     {
122         return _extensionClasses;
123     }
124 
125     /**
126      * Get the maxIdleTime.
127      *
128      * @return the maxIdleTime
129      */
130     public long getMaxIdleTime()
131     {
132         return _maxIdleTime;
133     }
134 
135     /**
136      * Set the maxIdleTime.
137      *
138      * @param maxIdleTime the maxIdleTime to set
139      */
140     public void setMaxIdleTime(int maxIdleTime)
141     {
142         _maxIdleTime = maxIdleTime;
143     }
144 
145     /**
146      * Get the bufferSize.
147      *
148      * @return the bufferSize
149      */
150     public int getBufferSize()
151     {
152         return _buffers.getBufferSize();
153     }
154 
155     /**
156      * Set the bufferSize.
157      *
158      * @param bufferSize the bufferSize to set
159      */
160     public void setBufferSize(int bufferSize)
161     {
162         if (bufferSize != getBufferSize())
163             _buffers = new WebSocketBuffers(bufferSize);
164     }
165 
166     /**
167      * @return The initial maximum text message size (in characters) for a connection
168      */
169     public int getMaxTextMessageSize()
170     {
171         return _maxTextMessageSize;
172     }
173 
174     /**
175      * Set the initial maximum text message size for a connection. This can be changed by
176      * the application calling {@link WebSocket.Connection#setMaxTextMessageSize(int)}.
177      * @param maxTextMessageSize The default maximum text message size (in characters) for a connection
178      */
179     public void setMaxTextMessageSize(int maxTextMessageSize)
180     {
181         _maxTextMessageSize = maxTextMessageSize;
182     }
183 
184     /**
185      * @return The initial maximum binary message size (in bytes)  for a connection
186      */
187     public int getMaxBinaryMessageSize()
188     {
189         return _maxBinaryMessageSize;
190     }
191 
192     /**
193      * Set the initial maximum binary message size for a connection. This can be changed by
194      * the application calling {@link WebSocket.Connection#setMaxBinaryMessageSize(int)}.
195      * @param maxBinaryMessageSize The default maximum binary message size (in bytes) for a connection
196      */
197     public void setMaxBinaryMessageSize(int maxBinaryMessageSize)
198     {
199         _maxBinaryMessageSize = maxBinaryMessageSize;
200     }
201 
202     @Override
203     protected void doStop() throws Exception
204     {
205         closeConnections();
206     }
207 
208     /**
209      * Upgrade the request/response to a WebSocket Connection.
210      * <p>This method will not normally return, but will instead throw a
211      * UpgradeConnectionException, to exit HTTP handling and initiate
212      * WebSocket handling of the connection.
213      *
214      * @param request   The request to upgrade
215      * @param response  The response to upgrade
216      * @param websocket The websocket handler implementation to use
217      * @param protocol  The websocket protocol
218      * @throws IOException in case of I/O errors
219      */
220     public void upgrade(HttpServletRequest request, HttpServletResponse response, WebSocket websocket, String protocol)
221             throws IOException
222     {
223         if (!"websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
224             throw new IllegalStateException("!Upgrade:websocket");
225         if (!"HTTP/1.1".equals(request.getProtocol()))
226             throw new IllegalStateException("!HTTP/1.1");
227 
228         int draft = request.getIntHeader("Sec-WebSocket-Version");
229         if (draft < 0) {
230             // Old pre-RFC version specifications (header not present in RFC-6455)
231             draft = request.getIntHeader("Sec-WebSocket-Draft");
232         }
233         // Remember requested version for possible error message later
234         int requestedVersion = draft;
235         AbstractHttpConnection http = AbstractHttpConnection.getCurrentConnection();
236         if (http instanceof BlockingHttpConnection)
237             throw new IllegalStateException("Websockets not supported on blocking connectors");
238         ConnectedEndPoint endp = (ConnectedEndPoint)http.getEndPoint();
239 
240         List<String> extensions_requested = new ArrayList<String>();
241         @SuppressWarnings("unchecked")
242         Enumeration<String> e = request.getHeaders("Sec-WebSocket-Extensions");
243         while (e.hasMoreElements())
244         {
245             QuotedStringTokenizer tok = new QuotedStringTokenizer(e.nextElement(),",");
246             while (tok.hasMoreTokens())
247             {
248                 extensions_requested.add(tok.nextToken());
249             }
250         }
251 
252         final WebSocketServletConnection connection;
253         if (draft<_minVersion)
254             draft=Integer.MAX_VALUE;
255         switch (draft)
256         {
257             case -1: // unspecified draft/version (such as early OSX Safari 5.1 and iOS 5.x)
258             case 0: // Old school draft/version
259             {
260                 connection = new WebSocketServletConnectionD00(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
261                 break;
262             }
263             case 1:
264             case 2:
265             case 3:
266             case 4:
267             case 5:
268             case 6:
269             {
270                 connection = new WebSocketServletConnectionD06(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
271                 break;
272             }
273             case 7:
274             case 8:
275             {
276                 List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionD08.OP_EXT_DATA, 16 - WebSocketConnectionD08.OP_EXT_CTRL, 3);
277                 connection = new WebSocketServletConnectionD08(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
278                 break;
279             }
280             case WebSocketConnectionRFC6455.VERSION: // RFC 6455 Version
281             {
282                 List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionRFC6455.OP_EXT_DATA, 16 - WebSocketConnectionRFC6455.OP_EXT_CTRL, 3);
283                 connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
284                 break;
285             }
286             default:
287             {
288                 // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol
289                 // Using the examples as outlined
290                 String versions="13";
291                 if (_minVersion<=8)
292                     versions+=", 8";
293                 if (_minVersion<=6)
294                     versions+=", 6";
295                 if (_minVersion<=0)
296                     versions+=", 0";
297                     
298                 response.setHeader("Sec-WebSocket-Version", versions);
299 
300                 // Make error clear for developer / end-user
301                 StringBuilder err = new StringBuilder();
302                 err.append("Unsupported websocket client version specification ");
303                 if(requestedVersion >= 0) {
304                     err.append("[").append(requestedVersion).append("]");
305                 } else {
306                     err.append("<Unspecified, likely a pre-draft version of websocket>");
307                 }
308                 err.append(", configured minVersion [").append(_minVersion).append("]");
309                 err.append(", reported supported versions [").append(versions).append("]");
310                 LOG.warn(err.toString()); // Log it
311                 // use spec language for unsupported versions
312                 throw new HttpException(400, "Unsupported websocket version specification"); // Tell client
313             }
314         }
315 
316         addConnection(connection);
317 
318         // Set the defaults
319         connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize);
320         connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize);
321 
322         // Let the connection finish processing the handshake
323         connection.handshake(request, response, protocol);
324         response.flushBuffer();
325 
326         // Give the connection any unused data from the HTTP connection.
327         connection.fillBuffersFrom(((HttpParser)http.getParser()).getHeaderBuffer());
328         connection.fillBuffersFrom(((HttpParser)http.getParser()).getBodyBuffer());
329 
330         // Tell jetty about the new connection
331         LOG.debug("Websocket upgrade {} {} {} {}",request.getRequestURI(),draft,protocol,connection);
332         request.setAttribute("org.eclipse.jetty.io.Connection", connection);
333     }
334 
335     protected String[] parseProtocols(String protocol)
336     {
337         if (protocol == null)
338             return new String[]{null};
339         protocol = protocol.trim();
340         if (protocol == null || protocol.length() == 0)
341             return new String[]{null};
342         String[] passed = protocol.split("\\s*,\\s*");
343         String[] protocols = new String[passed.length + 1];
344         System.arraycopy(passed, 0, protocols, 0, passed.length);
345         return protocols;
346     }
347 
348     public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response)
349             throws IOException
350     {
351         if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
352         {
353             String origin = request.getHeader("Origin");
354             if (origin==null)
355                 origin = request.getHeader("Sec-WebSocket-Origin");
356             if (!_acceptor.checkOrigin(request,origin))
357             {
358                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
359                 return false;
360             }
361 
362             // Try each requested protocol
363             WebSocket websocket = null;
364 
365             @SuppressWarnings("unchecked")
366             Enumeration<String> protocols = request.getHeaders("Sec-WebSocket-Protocol");
367             String protocol=null;
368             while (protocol==null && protocols!=null && protocols.hasMoreElements())
369             {
370                 String candidate = protocols.nextElement();
371                 for (String p : parseProtocols(candidate))
372                 {
373                     websocket = _acceptor.doWebSocketConnect(request, p);
374                     if (websocket != null)
375                     {
376                         protocol = p;
377                         break;
378                     }
379                 }
380             }
381 
382             // Did we get a websocket?
383             if (websocket == null)
384             {
385                 // Try with no protocol
386                 websocket = _acceptor.doWebSocketConnect(request, null);
387 
388                 if (websocket==null)
389                 {
390                     response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
391                     return false;
392                 }
393             }
394 
395             // Send the upgrade
396             upgrade(request, response, websocket, protocol);
397             return true;
398         }
399 
400         return false;
401     }
402 
403     public List<Extension> initExtensions(List<String> requested,int maxDataOpcodes,int maxControlOpcodes,int maxReservedBits)
404     {
405         List<Extension> extensions = new ArrayList<Extension>();
406         for (String rExt : requested)
407         {
408             QuotedStringTokenizer tok = new QuotedStringTokenizer(rExt,";");
409             String extName=tok.nextToken().trim();
410             Map<String,String> parameters = new HashMap<String,String>();
411             while (tok.hasMoreTokens())
412             {
413                 QuotedStringTokenizer nv = new QuotedStringTokenizer(tok.nextToken().trim(),"=");
414                 String name=nv.nextToken().trim();
415                 String value=nv.hasMoreTokens()?nv.nextToken().trim():null;
416                 parameters.put(name,value);
417             }
418 
419             Extension extension = newExtension(extName);
420 
421             if (extension==null)
422                 continue;
423 
424             if (extension.init(parameters))
425             {
426                 LOG.debug("add {} {}",extName,parameters);
427                 extensions.add(extension);
428             }
429         }
430         LOG.debug("extensions={}",extensions);
431         return extensions;
432     }
433 
434     private Extension newExtension(String name)
435     {
436         try
437         {
438             Class<? extends Extension> extClass = _extensionClasses.get(name);
439             if (extClass!=null)
440                 return extClass.newInstance();
441         }
442         catch (Exception e)
443         {
444             LOG.warn(e);
445         }
446 
447         return null;
448     }
449 
450     protected boolean addConnection(WebSocketServletConnection connection)
451     {
452         return isRunning() && connections.add(connection);
453     }
454 
455     protected boolean removeConnection(WebSocketServletConnection connection)
456     {
457         return connections.remove(connection);
458     }
459 
460     protected void closeConnections()
461     {
462         for (WebSocketServletConnection connection : connections)
463             connection.shutdown();
464     }
465 }