View Javadoc

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  
19  package org.eclipse.jetty.websocket.server;
20  
21  import java.io.IOException;
22  import java.net.URI;
23  import java.net.URISyntaxException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.concurrent.CopyOnWriteArraySet;
32  import java.util.concurrent.Executor;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  
36  import org.eclipse.jetty.http.HttpStatus;
37  import org.eclipse.jetty.io.ByteBufferPool;
38  import org.eclipse.jetty.io.EndPoint;
39  import org.eclipse.jetty.io.MappedByteBufferPool;
40  import org.eclipse.jetty.server.HttpConnection;
41  import org.eclipse.jetty.util.component.ContainerLifeCycle;
42  import org.eclipse.jetty.util.log.Log;
43  import org.eclipse.jetty.util.log.Logger;
44  import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
45  import org.eclipse.jetty.util.thread.Scheduler;
46  import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
47  import org.eclipse.jetty.websocket.api.WebSocketException;
48  import org.eclipse.jetty.websocket.api.WebSocketPolicy;
49  import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
50  import org.eclipse.jetty.websocket.api.util.QuoteUtil;
51  import org.eclipse.jetty.websocket.common.LogicalConnection;
52  import org.eclipse.jetty.websocket.common.SessionFactory;
53  import org.eclipse.jetty.websocket.common.SessionListener;
54  import org.eclipse.jetty.websocket.common.WebSocketSession;
55  import org.eclipse.jetty.websocket.common.WebSocketSessionFactory;
56  import org.eclipse.jetty.websocket.common.events.EventDriver;
57  import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
58  import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
59  import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory;
60  import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
61  import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
62  import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
63  import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
64  
65  /**
66   * Factory to create WebSocket connections
67   */
68  public class WebSocketServerFactory extends ContainerLifeCycle implements WebSocketCreator, WebSocketServletFactory, SessionListener
69  {
70      private static final Logger LOG = Log.getLogger(WebSocketServerFactory.class);
71  
72      private final Map<Integer, WebSocketHandshake> handshakes = new HashMap<>();
73      /**
74       * Have the factory maintain 1 and only 1 scheduler. All connections share this scheduler.
75       */
76      private final Scheduler scheduler = new ScheduledExecutorScheduler();
77      private final String supportedVersions;
78      private final WebSocketPolicy defaultPolicy;
79      private final EventDriverFactory eventDriverFactory;
80      private final ByteBufferPool bufferPool;
81      private final WebSocketExtensionFactory extensionFactory;
82      private List<SessionFactory> sessionFactories;
83      private Set<WebSocketSession> openSessions = new CopyOnWriteArraySet<>();
84      private WebSocketCreator creator;
85      private List<Class<?>> registeredSocketClasses;
86  
87      public WebSocketServerFactory()
88      {
89          this(WebSocketPolicy.newServerPolicy(), new MappedByteBufferPool());
90      }
91  
92      public WebSocketServerFactory(WebSocketPolicy policy)
93      {
94          this(policy, new MappedByteBufferPool());
95      }
96  
97      public WebSocketServerFactory(ByteBufferPool bufferPool)
98      {
99          this(WebSocketPolicy.newServerPolicy(), bufferPool);
100     }
101 
102     public WebSocketServerFactory(WebSocketPolicy policy, ByteBufferPool bufferPool)
103     {
104         handshakes.put(HandshakeRFC6455.VERSION, new HandshakeRFC6455());
105 
106         addBean(scheduler);
107         addBean(bufferPool);
108 
109         this.registeredSocketClasses = new ArrayList<>();
110 
111         this.defaultPolicy = policy;
112         this.eventDriverFactory = new EventDriverFactory(defaultPolicy);
113         this.bufferPool = bufferPool;
114         this.extensionFactory = new WebSocketExtensionFactory(defaultPolicy, this.bufferPool);
115         this.sessionFactories = new ArrayList<>();
116         this.sessionFactories.add(new WebSocketSessionFactory(this));
117         this.creator = this;
118 
119         // Create supportedVersions
120         List<Integer> versions = new ArrayList<>();
121         for (int v : handshakes.keySet())
122         {
123             versions.add(v);
124         }
125         Collections.sort(versions, Collections.reverseOrder()); // newest first
126         StringBuilder rv = new StringBuilder();
127         for (int v : versions)
128         {
129             if (rv.length() > 0)
130             {
131                 rv.append(", ");
132             }
133             rv.append(v);
134         }
135         supportedVersions = rv.toString();
136     }
137 
138     @Override
139     public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException
140     {
141         return acceptWebSocket(getCreator(), request, response);
142     }
143 
144     @Override
145     public boolean acceptWebSocket(WebSocketCreator creator, HttpServletRequest request, HttpServletResponse response) throws IOException
146     {
147         try
148         {
149             ServletUpgradeRequest sockreq = new ServletUpgradeRequest(request);
150             ServletUpgradeResponse sockresp = new ServletUpgradeResponse(response);
151 
152             Object websocketPojo = creator.createWebSocket(sockreq, sockresp);
153 
154             // Handle response forbidden (and similar paths)
155             if (sockresp.isCommitted())
156             {
157                 return false;
158             }
159 
160             if (websocketPojo == null)
161             {
162                 // no creation, sorry
163                 sockresp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Endpoint Creation Failed");
164                 return false;
165             }
166 
167             // Send the upgrade
168             EventDriver driver = eventDriverFactory.wrap(websocketPojo);
169             return upgrade(sockreq, sockresp, driver);
170         }
171         catch (URISyntaxException e)
172         {
173             throw new IOException("Unable to accept websocket due to mangled URI", e);
174         }
175     }
176 
177     public void addSessionFactory(SessionFactory sessionFactory)
178     {
179         if (sessionFactories.contains(sessionFactory))
180         {
181             return;
182         }
183         this.sessionFactories.add(sessionFactory);
184     }
185 
186     @Override
187     public void cleanup()
188     {
189         try
190         {
191             this.stop();
192         }
193         catch (Exception e)
194         {
195             LOG.warn(e);
196         }
197     }
198 
199     protected void closeAllConnections()
200     {
201         for (WebSocketSession session : openSessions)
202         {
203             session.close();
204         }
205         openSessions.clear();
206     }
207 
208     @Override
209     public WebSocketServletFactory createFactory(WebSocketPolicy policy)
210     {
211         return new WebSocketServerFactory(policy, bufferPool);
212     }
213 
214     private WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection)
215     {
216         if (websocket == null)
217         {
218             throw new InvalidWebSocketException("Unable to create Session from null websocket");
219         }
220 
221         for (SessionFactory impl : sessionFactories)
222         {
223             if (impl.supports(websocket))
224             {
225                 try
226                 {
227                     return impl.createSession(requestURI, websocket, connection);
228                 }
229                 catch (Throwable e)
230                 {
231                     throw new InvalidWebSocketException("Unable to create Session", e);
232                 }
233             }
234         }
235 
236         throw new InvalidWebSocketException("Unable to create Session: unrecognized internal EventDriver type: " + websocket.getClass().getName());
237     }
238 
239     /**
240      * Default Creator logic
241      */
242     @Override
243     public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
244     {
245         if (registeredSocketClasses.size() < 1)
246         {
247             throw new WebSocketException("No WebSockets have been registered with the factory.  Cannot use default implementation of WebSocketCreator.");
248         }
249 
250         if (registeredSocketClasses.size() > 1)
251         {
252             LOG.warn("You have registered more than 1 websocket object, and are using the default WebSocketCreator! Using first registered websocket.");
253         }
254 
255         Class<?> firstClass = registeredSocketClasses.get(0);
256         try
257         {
258             return firstClass.newInstance();
259         }
260         catch (InstantiationException | IllegalAccessException e)
261         {
262             throw new WebSocketException("Unable to create instance of " + firstClass, e);
263         }
264     }
265 
266     @Override
267     protected void doStop() throws Exception
268     {
269         closeAllConnections();
270         super.doStop();
271     }
272 
273     @Override
274     public WebSocketCreator getCreator()
275     {
276         return this.creator;
277     }
278 
279     public EventDriverFactory getEventDriverFactory()
280     {
281         return eventDriverFactory;
282     }
283 
284     @Override
285     public ExtensionFactory getExtensionFactory()
286     {
287         return extensionFactory;
288     }
289 
290     public Set<WebSocketSession> getOpenSessions()
291     {
292         return Collections.unmodifiableSet(this.openSessions);
293     }
294 
295     @Override
296     public WebSocketPolicy getPolicy()
297     {
298         return defaultPolicy;
299     }
300 
301     @Override
302     public void init() throws Exception
303     {
304         start(); // start lifecycle
305     }
306 
307     @Override
308     public boolean isUpgradeRequest(HttpServletRequest request, HttpServletResponse response)
309     {
310         if (!"GET".equalsIgnoreCase(request.getMethod()))
311         {
312             // not a "GET" request (not a websocket upgrade)
313             return false;
314         }
315 
316         String connection = request.getHeader("connection");
317         if (connection == null)
318         {
319             // no "Connection: upgrade" header present.
320             return false;
321         }
322 
323         // Test for "Upgrade" token
324         boolean foundUpgradeToken = false;
325         Iterator<String> iter = QuoteUtil.splitAt(connection, ",");
326         while (iter.hasNext())
327         {
328             String token = iter.next();
329             if ("upgrade".equalsIgnoreCase(token))
330             {
331                 foundUpgradeToken = true;
332                 break;
333             }
334         }
335 
336         if (!foundUpgradeToken)
337         {
338             return false;
339         }
340 
341         String upgrade = request.getHeader("Upgrade");
342         if (upgrade == null)
343         {
344             // no "Upgrade: websocket" header present.
345             return false;
346         }
347 
348         if (!"websocket".equalsIgnoreCase(upgrade))
349         {
350             LOG.debug("Not a 'Upgrade: WebSocket' (was [Upgrade: " + upgrade + "])");
351             return false;
352         }
353 
354         if (!"HTTP/1.1".equals(request.getProtocol()))
355         {
356             LOG.debug("Not a 'HTTP/1.1' request (was [" + request.getProtocol() + "])");
357             return false;
358         }
359 
360         return true;
361     }
362 
363     @Override
364     public void onSessionClosed(WebSocketSession session)
365     {
366         this.openSessions.remove(session);
367     }
368 
369     @Override
370     public void onSessionOpened(WebSocketSession session)
371     {
372         this.openSessions.add(session);
373     }
374 
375     protected String[] parseProtocols(String protocol)
376     {
377         if (protocol == null)
378         {
379             return new String[]{null};
380         }
381         protocol = protocol.trim();
382         if (protocol.length() == 0)
383         {
384             return new String[]{null};
385         }
386         String[] passed = protocol.split("\\s*,\\s*");
387         String[] protocols = new String[passed.length + 1];
388         System.arraycopy(passed, 0, protocols, 0, passed.length);
389         return protocols;
390     }
391 
392     @Override
393     public void register(Class<?> websocketPojo)
394     {
395         registeredSocketClasses.add(websocketPojo);
396     }
397 
398     @Override
399     public void setCreator(WebSocketCreator creator)
400     {
401         this.creator = creator;
402     }
403 
404     /**
405      * Upgrade the request/response to a WebSocket Connection.
406      * <p/>
407      * This method will not normally return, but will instead throw a UpgradeConnectionException, to exit HTTP handling and initiate WebSocket handling of the
408      * connection.
409      *
410      * @param request  The request to upgrade
411      * @param response The response to upgrade
412      * @param driver   The websocket handler implementation to use
413      * @throws IOException
414      */
415     public boolean upgrade(ServletUpgradeRequest request, ServletUpgradeResponse response, EventDriver driver) throws IOException
416     {
417         if (!"websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
418         {
419             throw new IllegalStateException("Not a 'WebSocket: Upgrade' request");
420         }
421         if (!"HTTP/1.1".equals(request.getHttpVersion()))
422         {
423             throw new IllegalStateException("Not a 'HTTP/1.1' request");
424         }
425 
426         int version = request.getHeaderInt("Sec-WebSocket-Version");
427         if (version < 0)
428         {
429             // Old pre-RFC version specifications (header not present in RFC-6455)
430             version = request.getHeaderInt("Sec-WebSocket-Draft");
431         }
432 
433         WebSocketHandshake handshaker = handshakes.get(version);
434         if (handshaker == null)
435         {
436             StringBuilder warn = new StringBuilder();
437             warn.append("Client ").append(request.getRemoteAddress());
438             warn.append(" (:").append(request.getRemotePort());
439             warn.append(") User Agent: ");
440             String ua = request.getHeader("User-Agent");
441             if (ua == null)
442             {
443                 warn.append("[unset] ");
444             }
445             else
446             {
447                 warn.append('"').append(ua.replaceAll("<", "&lt;")).append("\" ");
448             }
449             warn.append("requested WebSocket version [").append(version);
450             warn.append("], Jetty supports version");
451             if (handshakes.size() > 1)
452             {
453                 warn.append('s');
454             }
455             warn.append(": [").append(supportedVersions).append("]");
456             LOG.warn(warn.toString());
457 
458             // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol
459             // Using the examples as outlined
460             response.setHeader("Sec-WebSocket-Version", supportedVersions);
461             response.sendError(HttpStatus.BAD_REQUEST_400, "Unsupported websocket version specification");
462             return false;
463         }
464 
465         // Initialize / Negotiate Extensions
466         ExtensionStack extensionStack = new ExtensionStack(getExtensionFactory());
467         // The JSR allows for the extensions to be pre-negotiated, filtered, etc...
468         // Usually from a Configurator.
469         if (response.isExtensionsNegotiated())
470         {
471             // Use pre-negotiated extension list from response
472             extensionStack.negotiate(response.getExtensions());
473         }
474         else
475         {
476             // Use raw extension list from request
477             extensionStack.negotiate(request.getExtensions());
478         }
479 
480         // Create connection
481         HttpConnection http = HttpConnection.getCurrentConnection();
482         EndPoint endp = http.getEndPoint();
483         Executor executor = http.getConnector().getExecutor();
484         ByteBufferPool bufferPool = http.getConnector().getByteBufferPool();
485         WebSocketServerConnection wsConnection = new WebSocketServerConnection(endp, executor, scheduler, driver.getPolicy(), bufferPool);
486 
487         extensionStack.setPolicy(driver.getPolicy());
488         extensionStack.configure(wsConnection.getParser());
489         extensionStack.configure(wsConnection.getGenerator());
490 
491         LOG.debug("HttpConnection: {}", http);
492         LOG.debug("WebSocketConnection: {}", wsConnection);
493 
494         // Setup Session
495         WebSocketSession session = createSession(request.getRequestURI(), driver, wsConnection);
496         session.setPolicy(driver.getPolicy());
497         session.setUpgradeRequest(request);
498         // set true negotiated extension list back to response 
499         response.setExtensions(extensionStack.getNegotiatedExtensions());
500         session.setUpgradeResponse(response);
501         wsConnection.setSession(session);
502 
503         // Setup Incoming Routing
504         wsConnection.setNextIncomingFrames(extensionStack);
505         extensionStack.setNextIncoming(session);
506 
507         // Setup Outgoing Routing
508         session.setOutgoingHandler(extensionStack);
509         extensionStack.setNextOutgoing(wsConnection);
510 
511         // Start Components
512         try
513         {
514             session.start();
515         }
516         catch (Exception e)
517         {
518             throw new IOException("Unable to start Session", e);
519         }
520         try
521         {
522             extensionStack.start();
523         }
524         catch (Exception e)
525         {
526             throw new IOException("Unable to start Extension Stack", e);
527         }
528 
529         // Tell jetty about the new connection
530         request.setServletAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, wsConnection);
531 
532         // Process (version specific) handshake response
533         LOG.debug("Handshake Response: {}", handshaker);
534         handshaker.doHandshakeResponse(request, response);
535 
536         LOG.debug("Websocket upgrade {} {} {} {}", request.getRequestURI(), version, response.getAcceptedSubProtocol(), wsConnection);
537         return true;
538     }
539 }