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