blob: 04ab4f30630fcad812b6d7b7ebd67bbae0b9154d [file] [log] [blame]
Jake Slack03928ae2014-05-13 18:41:56 -07001//
2// ========================================================================
3// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4// ------------------------------------------------------------------------
5// All rights reserved. This program and the accompanying materials
6// are made available under the terms of the Eclipse Public License v1.0
7// and Apache License v2.0 which accompanies this distribution.
8//
9// The Eclipse Public License is available at
10// http://www.eclipse.org/legal/epl-v10.html
11//
12// The Apache License v2.0 is available at
13// http://www.opensource.org/licenses/apache2.0.php
14//
15// You may elect to redistribute this code under either of these licenses.
16// ========================================================================
17//
18
19package org.eclipse.jetty.websocket;
20
21import java.io.IOException;
22import java.util.ArrayList;
23import java.util.Enumeration;
24import java.util.HashMap;
25import java.util.List;
26import java.util.Map;
27import java.util.Queue;
28import java.util.concurrent.ConcurrentLinkedQueue;
29import javax.servlet.http.HttpServletRequest;
30import javax.servlet.http.HttpServletResponse;
31
32import org.eclipse.jetty.http.HttpException;
33import org.eclipse.jetty.http.HttpParser;
34import org.eclipse.jetty.io.ConnectedEndPoint;
35import org.eclipse.jetty.server.AbstractHttpConnection;
36import org.eclipse.jetty.server.BlockingHttpConnection;
37import org.eclipse.jetty.util.QuotedStringTokenizer;
38import org.eclipse.jetty.util.component.AbstractLifeCycle;
39import org.eclipse.jetty.util.log.Log;
40import org.eclipse.jetty.util.log.Logger;
41
42/**
43 * Factory to create WebSocket connections
44 */
45public class WebSocketFactory extends AbstractLifeCycle
46{
47 private static final Logger LOG = Log.getLogger(WebSocketFactory.class);
48 private final Queue<WebSocketServletConnection> connections = new ConcurrentLinkedQueue<WebSocketServletConnection>();
49
50 public interface Acceptor
51 {
52 /* ------------------------------------------------------------ */
53 /**
54 * <p>Factory method that applications needs to implement to return a
55 * {@link WebSocket} object.</p>
56 * @param request the incoming HTTP upgrade request
57 * @param protocol the websocket sub protocol
58 * @return a new {@link WebSocket} object that will handle websocket events.
59 */
60 WebSocket doWebSocketConnect(HttpServletRequest request, String protocol);
61
62 /* ------------------------------------------------------------ */
63 /**
64 * <p>Checks the origin of an incoming WebSocket handshake request.</p>
65 * @param request the incoming HTTP upgrade request
66 * @param origin the origin URI
67 * @return boolean to indicate that the origin is acceptable.
68 */
69 boolean checkOrigin(HttpServletRequest request, String origin);
70 }
71
72 private final Map<String,Class<? extends Extension>> _extensionClasses = new HashMap<String, Class<? extends Extension>>();
73 {
74 _extensionClasses.put("identity",IdentityExtension.class);
75 _extensionClasses.put("fragment",FragmentExtension.class);
76 _extensionClasses.put("x-deflate-frame",DeflateFrameExtension.class);
77 }
78
79 private final Acceptor _acceptor;
80 private WebSocketBuffers _buffers;
81 private int _maxIdleTime = 300000;
82 private int _maxTextMessageSize = 16 * 1024;
83 private int _maxBinaryMessageSize = -1;
84 private int _minVersion;
85
86 public WebSocketFactory(Acceptor acceptor)
87 {
88 this(acceptor, 64 * 1024, WebSocketConnectionRFC6455.VERSION);
89 }
90
91 public WebSocketFactory(Acceptor acceptor, int bufferSize)
92 {
93 this(acceptor, bufferSize, WebSocketConnectionRFC6455.VERSION);
94 }
95
96 public WebSocketFactory(Acceptor acceptor, int bufferSize, int minVersion)
97 {
98 _buffers = new WebSocketBuffers(bufferSize);
99 _acceptor = acceptor;
100 _minVersion=WebSocketConnectionRFC6455.VERSION;
101 }
102
103 public int getMinVersion()
104 {
105 return _minVersion;
106 }
107
108 /* ------------------------------------------------------------ */
109 /**
110 * @param minVersion The minimum support version (default RCF6455.VERSION == 13 )
111 */
112 public void setMinVersion(int minVersion)
113 {
114 _minVersion = minVersion;
115 }
116
117 /**
118 * @return A modifiable map of extension name to extension class
119 */
120 public Map<String,Class<? extends Extension>> getExtensionClassesMap()
121 {
122 return _extensionClasses;
123 }
124
125 /**
126 * Get the maxIdleTime.
127 *
128 * @return the maxIdleTime
129 */
130 public long getMaxIdleTime()
131 {
132 return _maxIdleTime;
133 }
134
135 /**
136 * Set the maxIdleTime.
137 *
138 * @param maxIdleTime the maxIdleTime to set
139 */
140 public void setMaxIdleTime(int maxIdleTime)
141 {
142 _maxIdleTime = maxIdleTime;
143 }
144
145 /**
146 * Get the bufferSize.
147 *
148 * @return the bufferSize
149 */
150 public int getBufferSize()
151 {
152 return _buffers.getBufferSize();
153 }
154
155 /**
156 * Set the bufferSize.
157 *
158 * @param bufferSize the bufferSize to set
159 */
160 public void setBufferSize(int bufferSize)
161 {
162 if (bufferSize != getBufferSize())
163 _buffers = new WebSocketBuffers(bufferSize);
164 }
165
166 /**
167 * @return The initial maximum text message size (in characters) for a connection
168 */
169 public int getMaxTextMessageSize()
170 {
171 return _maxTextMessageSize;
172 }
173
174 /**
175 * Set the initial maximum text message size for a connection. This can be changed by
176 * the application calling {@link WebSocket.Connection#setMaxTextMessageSize(int)}.
177 * @param maxTextMessageSize The default maximum text message size (in characters) for a connection
178 */
179 public void setMaxTextMessageSize(int maxTextMessageSize)
180 {
181 _maxTextMessageSize = maxTextMessageSize;
182 }
183
184 /**
185 * @return The initial maximum binary message size (in bytes) for a connection
186 */
187 public int getMaxBinaryMessageSize()
188 {
189 return _maxBinaryMessageSize;
190 }
191
192 /**
193 * Set the initial maximum binary message size for a connection. This can be changed by
194 * the application calling {@link WebSocket.Connection#setMaxBinaryMessageSize(int)}.
195 * @param maxBinaryMessageSize The default maximum binary message size (in bytes) for a connection
196 */
197 public void setMaxBinaryMessageSize(int maxBinaryMessageSize)
198 {
199 _maxBinaryMessageSize = maxBinaryMessageSize;
200 }
201
202 @Override
203 protected void doStop() throws Exception
204 {
205 closeConnections();
206 }
207
208 /**
209 * Upgrade the request/response to a WebSocket Connection.
210 * <p>This method will not normally return, but will instead throw a
211 * UpgradeConnectionException, to exit HTTP handling and initiate
212 * WebSocket handling of the connection.
213 *
214 * @param request The request to upgrade
215 * @param response The response to upgrade
216 * @param websocket The websocket handler implementation to use
217 * @param protocol The websocket protocol
218 * @throws IOException in case of I/O errors
219 */
220 public void upgrade(HttpServletRequest request, HttpServletResponse response, WebSocket websocket, String protocol)
221 throws IOException
222 {
223 if (!"websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
224 throw new IllegalStateException("!Upgrade:websocket");
225 if (!"HTTP/1.1".equals(request.getProtocol()))
226 throw new IllegalStateException("!HTTP/1.1");
227
228 int draft = request.getIntHeader("Sec-WebSocket-Version");
229 if (draft < 0) {
230 // Old pre-RFC version specifications (header not present in RFC-6455)
231 draft = request.getIntHeader("Sec-WebSocket-Draft");
232 }
233 // Remember requested version for possible error message later
234 int requestedVersion = draft;
235 AbstractHttpConnection http = AbstractHttpConnection.getCurrentConnection();
236 if (http instanceof BlockingHttpConnection)
237 throw new IllegalStateException("Websockets not supported on blocking connectors");
238 ConnectedEndPoint endp = (ConnectedEndPoint)http.getEndPoint();
239
240 List<String> extensions_requested = new ArrayList<String>();
241 @SuppressWarnings("unchecked")
242 Enumeration<String> e = request.getHeaders("Sec-WebSocket-Extensions");
243 while (e.hasMoreElements())
244 {
245 QuotedStringTokenizer tok = new QuotedStringTokenizer(e.nextElement(),",");
246 while (tok.hasMoreTokens())
247 {
248 extensions_requested.add(tok.nextToken());
249 }
250 }
251
252 final WebSocketServletConnection connection;
253 if (draft<_minVersion)
254 draft=Integer.MAX_VALUE;
255 switch (draft)
256 {
257 case -1: // unspecified draft/version (such as early OSX Safari 5.1 and iOS 5.x)
258 case 0: // Old school draft/version
259 {
260 connection = new WebSocketServletConnectionD00(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
261 break;
262 }
263 case 1:
264 case 2:
265 case 3:
266 case 4:
267 case 5:
268 case 6:
269 {
270 connection = new WebSocketServletConnectionD06(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol);
271 break;
272 }
273 case 7:
274 case 8:
275 {
276 List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionD08.OP_EXT_DATA, 16 - WebSocketConnectionD08.OP_EXT_CTRL, 3);
277 connection = new WebSocketServletConnectionD08(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
278 break;
279 }
280 case WebSocketConnectionRFC6455.VERSION: // RFC 6455 Version
281 {
282 List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionRFC6455.OP_EXT_DATA, 16 - WebSocketConnectionRFC6455.OP_EXT_CTRL, 3);
283 connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
284 break;
285 }
286 default:
287 {
288 // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol
289 // Using the examples as outlined
290 String versions="13";
291 if (_minVersion<=8)
292 versions+=", 8";
293 if (_minVersion<=6)
294 versions+=", 6";
295 if (_minVersion<=0)
296 versions+=", 0";
297
298 response.setHeader("Sec-WebSocket-Version", versions);
299
300 // Make error clear for developer / end-user
301 StringBuilder err = new StringBuilder();
302 err.append("Unsupported websocket client version specification ");
303 if(requestedVersion >= 0) {
304 err.append("[").append(requestedVersion).append("]");
305 } else {
306 err.append("<Unspecified, likely a pre-draft version of websocket>");
307 }
308 err.append(", configured minVersion [").append(_minVersion).append("]");
309 err.append(", reported supported versions [").append(versions).append("]");
310 LOG.warn(err.toString()); // Log it
311 // use spec language for unsupported versions
312 throw new HttpException(400, "Unsupported websocket version specification"); // Tell client
313 }
314 }
315
316 addConnection(connection);
317
318 // Set the defaults
319 connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize);
320 connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize);
321
322 // Let the connection finish processing the handshake
323 connection.handshake(request, response, protocol);
324 response.flushBuffer();
325
326 // Give the connection any unused data from the HTTP connection.
327 connection.fillBuffersFrom(((HttpParser)http.getParser()).getHeaderBuffer());
328 connection.fillBuffersFrom(((HttpParser)http.getParser()).getBodyBuffer());
329
330 // Tell jetty about the new connection
331 LOG.debug("Websocket upgrade {} {} {} {}",request.getRequestURI(),draft,protocol,connection);
332 request.setAttribute("org.eclipse.jetty.io.Connection", connection);
333 }
334
335 protected String[] parseProtocols(String protocol)
336 {
337 if (protocol == null)
338 return new String[]{null};
339 protocol = protocol.trim();
340 if (protocol == null || protocol.length() == 0)
341 return new String[]{null};
342 String[] passed = protocol.split("\\s*,\\s*");
343 String[] protocols = new String[passed.length + 1];
344 System.arraycopy(passed, 0, protocols, 0, passed.length);
345 return protocols;
346 }
347
348 public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response)
349 throws IOException
350 {
351 if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
352 {
353 String origin = request.getHeader("Origin");
354 if (origin==null)
355 origin = request.getHeader("Sec-WebSocket-Origin");
356 if (!_acceptor.checkOrigin(request,origin))
357 {
358 response.sendError(HttpServletResponse.SC_FORBIDDEN);
359 return false;
360 }
361
362 // Try each requested protocol
363 WebSocket websocket = null;
364
365 @SuppressWarnings("unchecked")
366 Enumeration<String> protocols = request.getHeaders("Sec-WebSocket-Protocol");
367 String protocol=null;
368 while (protocol==null && protocols!=null && protocols.hasMoreElements())
369 {
370 String candidate = protocols.nextElement();
371 for (String p : parseProtocols(candidate))
372 {
373 websocket = _acceptor.doWebSocketConnect(request, p);
374 if (websocket != null)
375 {
376 protocol = p;
377 break;
378 }
379 }
380 }
381
382 // Did we get a websocket?
383 if (websocket == null)
384 {
385 // Try with no protocol
386 websocket = _acceptor.doWebSocketConnect(request, null);
387
388 if (websocket==null)
389 {
390 response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
391 return false;
392 }
393 }
394
395 // Send the upgrade
396 upgrade(request, response, websocket, protocol);
397 return true;
398 }
399
400 return false;
401 }
402
403 public List<Extension> initExtensions(List<String> requested,int maxDataOpcodes,int maxControlOpcodes,int maxReservedBits)
404 {
405 List<Extension> extensions = new ArrayList<Extension>();
406 for (String rExt : requested)
407 {
408 QuotedStringTokenizer tok = new QuotedStringTokenizer(rExt,";");
409 String extName=tok.nextToken().trim();
410 Map<String,String> parameters = new HashMap<String,String>();
411 while (tok.hasMoreTokens())
412 {
413 QuotedStringTokenizer nv = new QuotedStringTokenizer(tok.nextToken().trim(),"=");
414 String name=nv.nextToken().trim();
415 String value=nv.hasMoreTokens()?nv.nextToken().trim():null;
416 parameters.put(name,value);
417 }
418
419 Extension extension = newExtension(extName);
420
421 if (extension==null)
422 continue;
423
424 if (extension.init(parameters))
425 {
426 LOG.debug("add {} {}",extName,parameters);
427 extensions.add(extension);
428 }
429 }
430 LOG.debug("extensions={}",extensions);
431 return extensions;
432 }
433
434 private Extension newExtension(String name)
435 {
436 try
437 {
438 Class<? extends Extension> extClass = _extensionClasses.get(name);
439 if (extClass!=null)
440 return extClass.newInstance();
441 }
442 catch (Exception e)
443 {
444 LOG.warn(e);
445 }
446
447 return null;
448 }
449
450 protected boolean addConnection(WebSocketServletConnection connection)
451 {
452 return isRunning() && connections.add(connection);
453 }
454
455 protected boolean removeConnection(WebSocketServletConnection connection)
456 {
457 return connections.remove(connection);
458 }
459
460 protected void closeConnections()
461 {
462 for (WebSocketServletConnection connection : connections)
463 connection.shutdown();
464 }
465}