1// 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} 466