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