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