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