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 6:
217                 extensions=Collections.emptyList();
218                 connection = new WebSocketConnectionD06(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
219                 break;
220             case 7:
221             case 8:
222             case 9:
223             case 10:
224             case 11:
225             case 12:
226                 extensions= initExtensions(extensions_requested,8-WebSocketConnectionD12.OP_EXT_DATA, 16-WebSocketConnectionD13.OP_EXT_CTRL,3);
227                 connection = new WebSocketConnectionD12(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol,extensions,draft);
228                 break;
229             case 13:
230                 extensions= initExtensions(extensions_requested,8-WebSocketConnectionD13.OP_EXT_DATA, 16-WebSocketConnectionD13.OP_EXT_CTRL,3);
231                 connection = new WebSocketConnectionD13(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol,extensions,draft);
232                 break;
233             default:
234                 LOG.warn("Unsupported Websocket version: "+draft);
235                 throw new HttpException(400, "Unsupported draft specification: " + draft);
236         }
237 
238         // Set the defaults
239         connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize);
240         connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize);
241 
242         // Let the connection finish processing the handshake
243         connection.handshake(request, response, protocol);
244         response.flushBuffer();
245 
246         // Give the connection any unused data from the HTTP connection.
247         connection.fillBuffersFrom(((HttpParser)http.getParser()).getHeaderBuffer());
248         connection.fillBuffersFrom(((HttpParser)http.getParser()).getBodyBuffer());
249 
250         // Tell jetty about the new connection
251         request.setAttribute("org.eclipse.jetty.io.Connection", connection);
252     }
253 
254     protected String[] parseProtocols(String protocol)
255     {
256         if (protocol == null)
257             return new String[]{null};
258         protocol = protocol.trim();
259         if (protocol == null || protocol.length() == 0)
260             return new String[]{null};
261         String[] passed = protocol.split("\\s*,\\s*");
262         String[] protocols = new String[passed.length + 1];
263         System.arraycopy(passed, 0, protocols, 0, passed.length);
264         return protocols;
265     }
266 
267     public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response)
268             throws IOException
269     {
270         if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
271         {
272             String origin = request.getHeader("Origin");
273             if (origin==null)
274                 origin = request.getHeader("Sec-WebSocket-Origin");
275             if (!_acceptor.checkOrigin(request,origin))
276             {
277                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
278                 return false;
279             }
280 
281             // Try each requested protocol
282             WebSocket websocket = null;
283             String protocol = request.getHeader("Sec-WebSocket-Protocol");
284             if (protocol == null) // TODO remove once draft period is over
285                 protocol = request.getHeader("WebSocket-Protocol");
286             for (String p : parseProtocols(protocol))
287             {
288                 websocket = _acceptor.doWebSocketConnect(request, p);
289                 if (websocket != null)
290                 {
291                     protocol = p;
292                     break;
293                 }
294             }
295 
296             // Did we get a websocket?
297             if (websocket == null)
298             {
299                 response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
300                 return false;
301             }
302 
303             // Send the upgrade
304             upgrade(request, response, websocket, protocol);
305             return true;
306         }
307 
308         return false;
309     }
310 
311     public List<Extension> initExtensions(List<String> requested,int maxDataOpcodes,int maxControlOpcodes,int maxReservedBits)
312     {
313         List<Extension> extensions = new ArrayList<Extension>();
314         for (String rExt : requested)
315         {
316             QuotedStringTokenizer tok = new QuotedStringTokenizer(rExt,";");
317             String extName=tok.nextToken().trim();
318             Map<String,String> parameters = new HashMap<String,String>();
319             while (tok.hasMoreTokens())
320             {
321                 QuotedStringTokenizer nv = new QuotedStringTokenizer(tok.nextToken().trim(),"=");
322                 String name=nv.nextToken().trim();
323                 String value=nv.hasMoreTokens()?nv.nextToken().trim():null;
324                 parameters.put(name,value);
325             }
326 
327             Extension extension = newExtension(extName);
328 
329             if (extension==null)
330                 continue;
331 
332             if (extension.init(parameters))
333             {
334                 LOG.debug("add {} {}",extName,parameters);
335                 extensions.add(extension);
336             }
337         }
338         LOG.debug("extensions={}",extensions);
339         return extensions;
340     }
341 
342     private Extension newExtension(String name)
343     {
344         try
345         {
346             Class<? extends Extension> extClass = _extensionClasses.get(name);
347             if (extClass!=null)
348                 return extClass.newInstance();
349         }
350         catch (Exception e)
351         {
352             LOG.warn(e);
353         }
354 
355         return null;
356     }
357 
358 
359 }