View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 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.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.concurrent.CopyOnWriteArrayList;
32  import java.util.concurrent.Executor;
33  import java.util.function.Consumer;
34  
35  import javax.servlet.ServletContext;
36  import javax.servlet.ServletException;
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpServletResponse;
39  
40  import org.eclipse.jetty.http.HttpStatus;
41  import org.eclipse.jetty.http.HttpVersion;
42  import org.eclipse.jetty.io.ByteBufferPool;
43  import org.eclipse.jetty.io.EndPoint;
44  import org.eclipse.jetty.io.MappedByteBufferPool;
45  import org.eclipse.jetty.server.ConnectionFactory;
46  import org.eclipse.jetty.server.Connector;
47  import org.eclipse.jetty.server.HttpConfiguration;
48  import org.eclipse.jetty.server.HttpConnection;
49  import org.eclipse.jetty.server.HttpConnectionFactory;
50  import org.eclipse.jetty.server.handler.ContextHandler;
51  import org.eclipse.jetty.servlet.ServletContextHandler;
52  import org.eclipse.jetty.util.DecoratedObjectFactory;
53  import org.eclipse.jetty.util.StringUtil;
54  import org.eclipse.jetty.util.component.ContainerLifeCycle;
55  import org.eclipse.jetty.util.log.Log;
56  import org.eclipse.jetty.util.log.Logger;
57  import org.eclipse.jetty.util.ssl.SslContextFactory;
58  import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
59  import org.eclipse.jetty.util.thread.Scheduler;
60  import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
61  import org.eclipse.jetty.websocket.api.WebSocketException;
62  import org.eclipse.jetty.websocket.api.WebSocketPolicy;
63  import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
64  import org.eclipse.jetty.websocket.api.util.QuoteUtil;
65  import org.eclipse.jetty.websocket.common.LogicalConnection;
66  import org.eclipse.jetty.websocket.common.SessionFactory;
67  import org.eclipse.jetty.websocket.common.WebSocketSession;
68  import org.eclipse.jetty.websocket.common.WebSocketSessionFactory;
69  import org.eclipse.jetty.websocket.common.events.EventDriver;
70  import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
71  import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
72  import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory;
73  import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection;
74  import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
75  import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
76  import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
77  import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
78  import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
79  
80  /**
81   * Factory to create WebSocket connections
82   */
83  public class WebSocketServerFactory extends ContainerLifeCycle implements WebSocketCreator, WebSocketContainerScope, WebSocketServletFactory
84  {
85      private static final Logger LOG = Log.getLogger(WebSocketServerFactory.class);
86  
87      private final ClassLoader contextClassloader;
88      private final Map<Integer, WebSocketHandshake> handshakes = new HashMap<>();
89      /**
90       * Have the factory maintain 1 and only 1 scheduler. All connections share this scheduler.
91       */
92      private final Scheduler scheduler = new ScheduledExecutorScheduler();
93      private final List<WebSocketSession.Listener> listeners = new CopyOnWriteArrayList<>();
94      private final String supportedVersions;
95      private final WebSocketPolicy defaultPolicy;
96      private final EventDriverFactory eventDriverFactory;
97      private final ByteBufferPool bufferPool;
98      private final WebSocketExtensionFactory extensionFactory;
99      private Executor executor;
100     private List<SessionFactory> sessionFactories;
101     private WebSocketCreator creator;
102     private List<Class<?>> registeredSocketClasses;
103     private DecoratedObjectFactory objectFactory;
104 
105     public WebSocketServerFactory()
106     {
107         this(WebSocketPolicy.newServerPolicy(), new MappedByteBufferPool());
108     }
109 
110     public WebSocketServerFactory(WebSocketPolicy policy)
111     {
112         this(policy, new MappedByteBufferPool());
113     }
114 
115     public WebSocketServerFactory(ByteBufferPool bufferPool)
116     {
117         this(WebSocketPolicy.newServerPolicy(), bufferPool);
118     }
119 
120     public WebSocketServerFactory(WebSocketPolicy policy, ByteBufferPool bufferPool)
121     {
122         handshakes.put(HandshakeRFC6455.VERSION, new HandshakeRFC6455());
123 
124         addBean(scheduler);
125         addBean(bufferPool);
126 
127         this.contextClassloader = Thread.currentThread().getContextClassLoader();
128 
129         this.registeredSocketClasses = new ArrayList<>();
130 
131         this.defaultPolicy = policy;
132         this.eventDriverFactory = new EventDriverFactory(defaultPolicy);
133         this.bufferPool = bufferPool;
134         this.extensionFactory = new WebSocketExtensionFactory(this);
135 
136         this.sessionFactories = new ArrayList<>();
137         this.sessionFactories.add(new WebSocketSessionFactory(this));
138         this.creator = this;
139 
140         // Create supportedVersions
141         List<Integer> versions = new ArrayList<>();
142         for (int v : handshakes.keySet())
143         {
144             versions.add(v);
145         }
146         Collections.sort(versions, Collections.reverseOrder()); // newest first
147         StringBuilder rv = new StringBuilder();
148         for (int v : versions)
149         {
150             if (rv.length() > 0)
151             {
152                 rv.append(", ");
153             }
154             rv.append(v);
155         }
156         supportedVersions = rv.toString();
157     }
158 
159     public void addSessionListener(WebSocketSession.Listener listener)
160     {
161         listeners.add(listener);
162     }
163 
164     public void removeSessionListener(WebSocketSession.Listener listener)
165     {
166         listeners.remove(listener);
167     }
168 
169     @Override
170     public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException
171     {
172         return acceptWebSocket(getCreator(), request, response);
173     }
174 
175     @Override
176     public boolean acceptWebSocket(WebSocketCreator creator, HttpServletRequest request, HttpServletResponse response) throws IOException
177     {
178         ClassLoader old = Thread.currentThread().getContextClassLoader();
179         try
180         {
181             Thread.currentThread().setContextClassLoader(contextClassloader);
182 
183             // Create Servlet Specific Upgrade Request/Response objects
184             ServletUpgradeRequest sockreq = new ServletUpgradeRequest(request);
185             ServletUpgradeResponse sockresp = new ServletUpgradeResponse(response);
186 
187             Object websocketPojo = creator.createWebSocket(sockreq, sockresp);
188 
189             // Handle response forbidden (and similar paths)
190             if (sockresp.isCommitted())
191             {
192                 return false;
193             }
194 
195             if (websocketPojo == null)
196             {
197                 // no creation, sorry
198                 sockresp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Endpoint Creation Failed");
199                 return false;
200             }
201 
202             // Allow Decorators to do their thing
203             websocketPojo = getObjectFactory().decorate(websocketPojo);
204 
205             // Get the original HTTPConnection
206             HttpConnection connection = (HttpConnection)request.getAttribute("org.eclipse.jetty.server.HttpConnection");
207 
208             // Send the upgrade
209             EventDriver driver = eventDriverFactory.wrap(websocketPojo);
210             return upgrade(connection, sockreq, sockresp, driver);
211         }
212         catch (URISyntaxException e)
213         {
214             throw new IOException("Unable to accept websocket due to mangled URI", e);
215         }
216         finally
217         {
218             Thread.currentThread().setContextClassLoader(old);
219         }
220     }
221 
222     public void addSessionFactory(SessionFactory sessionFactory)
223     {
224         if (sessionFactories.contains(sessionFactory))
225         {
226             return;
227         }
228         this.sessionFactories.add(sessionFactory);
229     }
230 
231     @Override
232     public void cleanup()
233     {
234         try
235         {
236             this.stop();
237         }
238         catch (Exception e)
239         {
240             LOG.warn(e);
241         }
242     }
243 
244     @Override
245     public WebSocketServletFactory createFactory(WebSocketPolicy policy)
246     {
247         return new WebSocketServerFactory(policy, bufferPool);
248     }
249 
250     private WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection)
251     {
252         if (websocket == null)
253         {
254             throw new InvalidWebSocketException("Unable to create Session from null websocket");
255         }
256 
257         for (SessionFactory impl : sessionFactories)
258         {
259             if (impl.supports(websocket))
260             {
261                 try
262                 {
263                     return impl.createSession(requestURI, websocket, connection);
264                 }
265                 catch (Throwable e)
266                 {
267                     throw new InvalidWebSocketException("Unable to create Session", e);
268                 }
269             }
270         }
271 
272         throw new InvalidWebSocketException("Unable to create Session: unrecognized internal EventDriver type: " + websocket.getClass().getName());
273     }
274 
275     /**
276      * Default Creator logic
277      */
278     @Override
279     public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
280     {
281         if (registeredSocketClasses.size() < 1)
282         {
283             throw new WebSocketException("No WebSockets have been registered with the factory.  Cannot use default implementation of WebSocketCreator.");
284         }
285 
286         if (registeredSocketClasses.size() > 1)
287         {
288             LOG.warn("You have registered more than 1 websocket object, and are using the default WebSocketCreator! Using first registered websocket.");
289         }
290 
291         Class<?> firstClass = registeredSocketClasses.get(0);
292         try
293         {
294             return objectFactory.createInstance(firstClass);
295         }
296         catch (InstantiationException | IllegalAccessException e)
297         {
298             throw new WebSocketException("Unable to create instance of " + firstClass, e);
299         }
300     }
301 
302     @Override
303     protected void doStart() throws Exception
304     {
305         if(this.objectFactory == null)
306         {
307             this.objectFactory = new DecoratedObjectFactory();
308         }
309 
310         super.doStart();
311     }
312 
313     @Override
314     public ByteBufferPool getBufferPool()
315     {
316         return this.bufferPool;
317     }
318 
319     @Override
320     public WebSocketCreator getCreator()
321     {
322         return this.creator;
323     }
324 
325     @Override
326     public Executor getExecutor()
327     {
328         return this.executor;
329     }
330 
331     public DecoratedObjectFactory getObjectFactory()
332     {
333         return objectFactory;
334     }
335 
336     public EventDriverFactory getEventDriverFactory()
337     {
338         return eventDriverFactory;
339     }
340 
341     @Override
342     public ExtensionFactory getExtensionFactory()
343     {
344         return extensionFactory;
345     }
346 
347     public Collection<WebSocketSession> getOpenSessions()
348     {
349         return getBeans(WebSocketSession.class);
350     }
351 
352     @Override
353     public WebSocketPolicy getPolicy()
354     {
355         return defaultPolicy;
356     }
357 
358     @Override
359     public SslContextFactory getSslContextFactory()
360     {
361         /* Not relevant for a Server, as this is defined in the
362          * Connector configuration
363          */
364         return null;
365     }
366 
367     public void init(ServletContextHandler context) throws ServletException
368     {
369         this.objectFactory = (DecoratedObjectFactory)context.getServletContext().getAttribute(DecoratedObjectFactory.ATTR);
370         if (this.objectFactory == null)
371         {
372             this.objectFactory = new DecoratedObjectFactory();
373         }
374 
375         this.executor = context.getServer().getThreadPool();
376     }
377 
378     @Override
379     public void init(ServletContext context) throws ServletException
380     {
381         // Setup ObjectFactory
382         this.objectFactory = (DecoratedObjectFactory)context.getAttribute(DecoratedObjectFactory.ATTR);
383         if (this.objectFactory == null)
384         {
385             this.objectFactory = new DecoratedObjectFactory();
386         }
387 
388         // Validate Environment
389         ContextHandler handler = ContextHandler.getContextHandler(context);
390 
391         if (handler == null)
392         {
393             throw new ServletException("Not running on Jetty, WebSocket support unavailable");
394         }
395 
396         this.executor = handler.getServer().getThreadPool();
397 
398         try
399         {
400             // start lifecycle
401             start();
402         }
403         catch (ServletException e)
404         {
405             throw e;
406         }
407         catch (Exception e)
408         {
409             throw new ServletException(e);
410         }
411     }
412 
413     @Override
414     public boolean isUpgradeRequest(HttpServletRequest request, HttpServletResponse response)
415     {
416         if (!"GET".equalsIgnoreCase(request.getMethod()))
417         {
418             // not a "GET" request (not a websocket upgrade)
419             return false;
420         }
421 
422         String connection = request.getHeader("connection");
423         if (connection == null)
424         {
425             // no "Connection: upgrade" header present.
426             return false;
427         }
428 
429         // Test for "Upgrade" token
430         boolean foundUpgradeToken = false;
431         Iterator<String> iter = QuoteUtil.splitAt(connection, ",");
432         while (iter.hasNext())
433         {
434             String token = iter.next();
435             if ("upgrade".equalsIgnoreCase(token))
436             {
437                 foundUpgradeToken = true;
438                 break;
439             }
440         }
441 
442         if (!foundUpgradeToken)
443         {
444             return false;
445         }
446 
447         String upgrade = request.getHeader("Upgrade");
448         if (upgrade == null)
449         {
450             // no "Upgrade: websocket" header present.
451             return false;
452         }
453 
454         if (!"websocket".equalsIgnoreCase(upgrade))
455         {
456             LOG.debug("Not a 'Upgrade: WebSocket' (was [Upgrade: " + upgrade + "])");
457             return false;
458         }
459 
460         if (!"HTTP/1.1".equals(request.getProtocol()))
461         {
462             LOG.debug("Not a 'HTTP/1.1' request (was [" + request.getProtocol() + "])");
463             return false;
464         }
465 
466         return true;
467     }
468 
469     @Override
470     public void onSessionOpened(WebSocketSession session)
471     {
472         addManaged(session);
473         notifySessionListeners(listener -> listener.onOpened(session));
474     }
475 
476     @Override
477     public void onSessionClosed(WebSocketSession session)
478     {
479         removeBean(session);
480         notifySessionListeners(listener -> listener.onClosed(session));
481     }
482 
483     private void notifySessionListeners(Consumer<WebSocketSession.Listener> consumer)
484     {
485         for (WebSocketSession.Listener listener : listeners)
486         {
487             try
488             {
489                 consumer.accept(listener);
490             }
491             catch (Throwable x)
492             {
493                 LOG.info("Exception while invoking listener " + listener, x);
494             }
495         }
496     }
497 
498     @Override
499     public void register(Class<?> websocketPojo)
500     {
501         registeredSocketClasses.add(websocketPojo);
502     }
503 
504     @Override
505     public void setCreator(WebSocketCreator creator)
506     {
507         this.creator = creator;
508     }
509 
510     /**
511      * Upgrade the request/response to a WebSocket Connection.
512      * <p/>
513      * This method will not normally return, but will instead throw a UpgradeConnectionException, to exit HTTP handling and initiate WebSocket handling of the
514      * connection.
515      *
516      * @param http     the raw http connection
517      * @param request  The request to upgrade
518      * @param response The response to upgrade
519      * @param driver   The websocket handler implementation to use
520      * @throws IOException
521      */
522     private boolean upgrade(HttpConnection http, ServletUpgradeRequest request, ServletUpgradeResponse response, EventDriver driver) throws IOException
523     {
524         if (!"websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
525         {
526             throw new IllegalStateException("Not a 'WebSocket: Upgrade' request");
527         }
528         if (!"HTTP/1.1".equals(request.getHttpVersion()))
529         {
530             throw new IllegalStateException("Not a 'HTTP/1.1' request");
531         }
532 
533         int version = request.getHeaderInt("Sec-WebSocket-Version");
534         if (version < 0)
535         {
536             // Old pre-RFC version specifications (header not present in RFC-6455)
537             version = request.getHeaderInt("Sec-WebSocket-Draft");
538         }
539 
540         WebSocketHandshake handshaker = handshakes.get(version);
541         if (handshaker == null)
542         {
543             StringBuilder warn = new StringBuilder();
544             warn.append("Client ").append(request.getRemoteAddress());
545             warn.append(" (:").append(request.getRemotePort());
546             warn.append(") User Agent: ");
547             String ua = request.getHeader("User-Agent");
548             if (ua == null)
549             {
550                 warn.append("[unset] ");
551             }
552             else
553             {
554                 warn.append('"').append(StringUtil.sanitizeXmlString(ua)).append("\" ");
555             }
556             warn.append("requested WebSocket version [").append(version);
557             warn.append("], Jetty supports version");
558             if (handshakes.size() > 1)
559             {
560                 warn.append('s');
561             }
562             warn.append(": [").append(supportedVersions).append("]");
563             LOG.warn(warn.toString());
564 
565             // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol
566             // Using the examples as outlined
567             response.setHeader("Sec-WebSocket-Version", supportedVersions);
568             response.sendError(HttpStatus.BAD_REQUEST_400, "Unsupported websocket version specification");
569             return false;
570         }
571 
572         // Initialize / Negotiate Extensions
573         ExtensionStack extensionStack = new ExtensionStack(getExtensionFactory());
574         // The JSR allows for the extensions to be pre-negotiated, filtered, etc...
575         // Usually from a Configurator.
576         if (response.isExtensionsNegotiated())
577         {
578             // Use pre-negotiated extension list from response
579             extensionStack.negotiate(response.getExtensions());
580         }
581         else
582         {
583             // Use raw extension list from request
584             extensionStack.negotiate(request.getExtensions());
585         }
586 
587         // Get original HTTP connection
588         EndPoint endp = http.getEndPoint();
589         Connector connector = http.getConnector();
590         Executor executor = connector.getExecutor();
591         ByteBufferPool bufferPool = connector.getByteBufferPool();
592 
593         // Setup websocket connection
594         AbstractWebSocketConnection wsConnection = new WebSocketServerConnection(endp, executor, scheduler, driver.getPolicy(), bufferPool);
595 
596         extensionStack.setPolicy(driver.getPolicy());
597         extensionStack.configure(wsConnection.getParser());
598         extensionStack.configure(wsConnection.getGenerator());
599 
600         if (LOG.isDebugEnabled())
601         {
602             LOG.debug("HttpConnection: {}", http);
603             LOG.debug("WebSocketConnection: {}", wsConnection);
604         }
605 
606         // Setup Session
607         WebSocketSession session = createSession(request.getRequestURI(), driver, wsConnection);
608         session.setPolicy(driver.getPolicy());
609         session.setUpgradeRequest(request);
610         // set true negotiated extension list back to response
611         response.setExtensions(extensionStack.getNegotiatedExtensions());
612         session.setUpgradeResponse(response);
613         wsConnection.addListener(session);
614 
615         // Setup Incoming Routing
616         wsConnection.setNextIncomingFrames(extensionStack);
617         extensionStack.setNextIncoming(session);
618 
619         // Setup Outgoing Routing
620         session.setOutgoingHandler(extensionStack);
621         extensionStack.setNextOutgoing(wsConnection);
622 
623         // Start Components
624         session.addManaged(extensionStack);
625         this.addManaged(session);
626 
627         if (session.isFailed())
628         {
629             throw new IOException("Session failed to start");
630         }
631 
632         // Tell jetty about the new upgraded connection
633         request.setServletAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, wsConnection);
634 
635         if (LOG.isDebugEnabled())
636             LOG.debug("Handshake Response: {}", handshaker);
637 
638         if (getSendServerVersion(connector))
639             response.setHeader("Server",HttpConfiguration.SERVER_VERSION);
640 
641         // Process (version specific) handshake response
642         handshaker.doHandshakeResponse(request, response);
643 
644         if (LOG.isDebugEnabled())
645             LOG.debug("Websocket upgrade {} {} {} {}", request.getRequestURI(), version, response.getAcceptedSubProtocol(), wsConnection);
646 
647         return true;
648     }
649 
650     private boolean getSendServerVersion(Connector connector)
651     {
652         ConnectionFactory connFactory = connector.getConnectionFactory(HttpVersion.HTTP_1_1.asString());
653         if (connFactory == null)
654             return false;
655 
656         if (connFactory instanceof HttpConnectionFactory)
657         {
658             HttpConfiguration httpConf = ((HttpConnectionFactory)connFactory).getHttpConfiguration();
659             if (httpConf != null)
660                 return httpConf.getSendServerVersion();
661         }
662         return false;
663     }
664 }