View Javadoc

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