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.common;
20  
21  import java.io.IOException;
22  import java.net.InetSocketAddress;
23  import java.net.URI;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.Executor;
28  
29  import org.eclipse.jetty.io.ByteBufferPool;
30  import org.eclipse.jetty.util.annotation.ManagedAttribute;
31  import org.eclipse.jetty.util.annotation.ManagedObject;
32  import org.eclipse.jetty.util.component.ContainerLifeCycle;
33  import org.eclipse.jetty.util.component.Dumpable;
34  import org.eclipse.jetty.util.log.Log;
35  import org.eclipse.jetty.util.log.Logger;
36  import org.eclipse.jetty.websocket.api.BatchMode;
37  import org.eclipse.jetty.websocket.api.CloseStatus;
38  import org.eclipse.jetty.websocket.api.RemoteEndpoint;
39  import org.eclipse.jetty.websocket.api.Session;
40  import org.eclipse.jetty.websocket.api.StatusCode;
41  import org.eclipse.jetty.websocket.api.SuspendToken;
42  import org.eclipse.jetty.websocket.api.UpgradeRequest;
43  import org.eclipse.jetty.websocket.api.UpgradeResponse;
44  import org.eclipse.jetty.websocket.api.WebSocketException;
45  import org.eclipse.jetty.websocket.api.WebSocketPolicy;
46  import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
47  import org.eclipse.jetty.websocket.api.extensions.Frame;
48  import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
49  import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
50  import org.eclipse.jetty.websocket.common.events.EventDriver;
51  import org.eclipse.jetty.websocket.common.io.IOState;
52  import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
53  
54  @ManagedObject("A Jetty WebSocket Session")
55  public class WebSocketSession extends ContainerLifeCycle implements Session, IncomingFrames, ConnectionStateListener
56  {
57      private static final Logger LOG = Log.getLogger(WebSocketSession.class);
58      private final URI requestURI;
59      private final EventDriver websocket;
60      private final LogicalConnection connection;
61      private final SessionListener[] sessionListeners;
62      private final Executor executor;
63      private ExtensionFactory extensionFactory;
64      private String protocolVersion;
65      private Map<String, String[]> parameterMap = new HashMap<>();
66      private WebSocketRemoteEndpoint remote;
67      private IncomingFrames incomingHandler;
68      private OutgoingFrames outgoingHandler;
69      private WebSocketPolicy policy;
70      private UpgradeRequest upgradeRequest;
71      private UpgradeResponse upgradeResponse;
72  
73      public WebSocketSession(URI requestURI, EventDriver websocket, LogicalConnection connection, SessionListener... sessionListeners)
74      {
75          if (requestURI == null)
76          {
77              throw new RuntimeException("Request URI cannot be null");
78          }
79  
80          this.requestURI = requestURI;
81          this.websocket = websocket;
82          this.connection = connection;
83          this.sessionListeners = sessionListeners;
84          this.executor = connection.getExecutor();
85          this.outgoingHandler = connection;
86          this.incomingHandler = websocket;
87          this.connection.getIOState().addListener(this);
88      }
89  
90      @Override
91      public void close()
92      {
93          this.close(StatusCode.NORMAL, null);
94      }
95  
96      @Override
97      public void close(CloseStatus closeStatus)
98      {
99          this.close(closeStatus.getCode(), closeStatus.getPhrase());
100     }
101 
102     @Override
103     public void close(int statusCode, String reason)
104     {
105         connection.close(statusCode, reason);
106         notifyClose(statusCode, reason);
107     }
108 
109     /**
110      * Harsh disconnect
111      */
112     @Override
113     public void disconnect()
114     {
115         connection.disconnect();
116 
117         // notify of harsh disconnect
118         notifyClose(StatusCode.NO_CLOSE, "Harsh disconnect");
119     }
120 
121     public void dispatch(Runnable runnable)
122     {
123         executor.execute(runnable);
124     }
125 
126     @Override
127     public void dump(Appendable out, String indent) throws IOException
128     {
129         dumpThis(out);
130         out.append(indent).append(" +- incomingHandler : ");
131         if (incomingHandler instanceof Dumpable)
132         {
133             ((Dumpable)incomingHandler).dump(out, indent + "    ");
134         }
135         else
136         {
137             out.append(incomingHandler.toString()).append(System.lineSeparator());
138         }
139 
140         out.append(indent).append(" +- outgoingHandler : ");
141         if (outgoingHandler instanceof Dumpable)
142         {
143             ((Dumpable)outgoingHandler).dump(out, indent + "    ");
144         }
145         else
146         {
147             out.append(outgoingHandler.toString()).append(System.lineSeparator());
148         }
149     }
150 
151     @Override
152     public boolean equals(Object obj)
153     {
154         if (this == obj)
155         {
156             return true;
157         }
158         if (obj == null)
159         {
160             return false;
161         }
162         if (getClass() != obj.getClass())
163         {
164             return false;
165         }
166         WebSocketSession other = (WebSocketSession)obj;
167         if (connection == null)
168         {
169             if (other.connection != null)
170             {
171                 return false;
172             }
173         }
174         else if (!connection.equals(other.connection))
175         {
176             return false;
177         }
178         return true;
179     }
180 
181     public ByteBufferPool getBufferPool()
182     {
183         return this.connection.getBufferPool();
184     }
185 
186     public LogicalConnection getConnection()
187     {
188         return connection;
189     }
190 
191     public ExtensionFactory getExtensionFactory()
192     {
193         return extensionFactory;
194     }
195 
196     /**
197      * The idle timeout in milliseconds
198      */
199     @Override
200     public long getIdleTimeout()
201     {
202         return connection.getMaxIdleTimeout();
203     }
204 
205     @ManagedAttribute(readonly = true)
206     public IncomingFrames getIncomingHandler()
207     {
208         return incomingHandler;
209     }
210 
211     @Override
212     public InetSocketAddress getLocalAddress()
213     {
214         return connection.getLocalAddress();
215     }
216 
217     @ManagedAttribute(readonly = true)
218     public OutgoingFrames getOutgoingHandler()
219     {
220         return outgoingHandler;
221     }
222 
223     @Override
224     public WebSocketPolicy getPolicy()
225     {
226         return policy;
227     }
228 
229     @Override
230     public String getProtocolVersion()
231     {
232         return protocolVersion;
233     }
234 
235     @Override
236     public RemoteEndpoint getRemote()
237     {
238         ConnectionState state = connection.getIOState().getConnectionState();
239 
240         if ((state == ConnectionState.OPEN) || (state == ConnectionState.CONNECTED))
241         {
242             return remote;
243         }
244 
245         throw new WebSocketException("RemoteEndpoint unavailable, current state [" + state + "], expecting [OPEN or CONNECTED]");
246     }
247 
248     @Override
249     public InetSocketAddress getRemoteAddress()
250     {
251         return remote.getInetSocketAddress();
252     }
253 
254     public URI getRequestURI()
255     {
256         return requestURI;
257     }
258 
259     @Override
260     public UpgradeRequest getUpgradeRequest()
261     {
262         return this.upgradeRequest;
263     }
264 
265     @Override
266     public UpgradeResponse getUpgradeResponse()
267     {
268         return this.upgradeResponse;
269     }
270 
271     @Override
272     public int hashCode()
273     {
274         final int prime = 31;
275         int result = 1;
276         result = (prime * result) + ((connection == null) ? 0 : connection.hashCode());
277         return result;
278     }
279 
280     /**
281      * Incoming Errors from Parser
282      */
283     @Override
284     public void incomingError(Throwable t)
285     {
286         if (connection.getIOState().isInputAvailable())
287         {
288             // Forward Errors to User WebSocket Object
289             websocket.incomingError(t);
290         }
291     }
292 
293     /**
294      * Incoming Raw Frames from Parser
295      */
296     @Override
297     public void incomingFrame(Frame frame)
298     {
299         if (connection.getIOState().isInputAvailable())
300         {
301             // Forward Frames Through Extension List
302             incomingHandler.incomingFrame(frame);
303         }
304     }
305 
306     @Override
307     public boolean isOpen()
308     {
309         if (this.connection == null)
310         {
311             return false;
312         }
313         return this.connection.isOpen();
314     }
315 
316     @Override
317     public boolean isSecure()
318     {
319         if (upgradeRequest == null)
320         {
321             throw new IllegalStateException("No valid UpgradeRequest yet");
322         }
323 
324         URI requestURI = upgradeRequest.getRequestURI();
325 
326         return "wss".equalsIgnoreCase(requestURI.getScheme());
327     }
328 
329     public void notifyClose(int statusCode, String reason)
330     {
331         websocket.onClose(new CloseInfo(statusCode, reason));
332     }
333 
334     public void notifyError(Throwable cause)
335     {
336         incomingError(cause);
337     }
338 
339     @SuppressWarnings("incomplete-switch")
340     @Override
341     public void onConnectionStateChange(ConnectionState state)
342     {
343         switch (state)
344         {
345             case CLOSING:
346                 // notify session listeners
347                 for (SessionListener listener : sessionListeners)
348                 {
349                     try
350                     {
351                         listener.onSessionClosed(this);
352                     }
353                     catch (Throwable t)
354                     {
355                         LOG.ignore(t);
356                     }
357                 }
358                 break;
359             case CLOSED:
360                 IOState ioState = this.connection.getIOState();
361                 // The session only cares about abnormal close, as we need to notify
362                 // the endpoint of this close scenario.
363                 if (ioState.wasAbnormalClose())
364                 {
365                     CloseInfo close = ioState.getCloseInfo();
366                     LOG.debug("Detected abnormal close: {}", close);
367                     // notify local endpoint
368                     notifyClose(close.getStatusCode(), close.getReason());
369                 }
370                 break;
371             case OPEN:
372                 // notify session listeners
373                 for (SessionListener listener : sessionListeners)
374                 {
375                     try
376                     {
377                         listener.onSessionOpened(this);
378                     }
379                     catch (Throwable t)
380                     {
381                         LOG.ignore(t);
382                     }
383                 }
384                 break;
385         }
386     }
387 
388     /**
389      * Open/Activate the session
390      */
391     public void open()
392     {
393         if (remote != null)
394         {
395             // already opened
396             return;
397         }
398 
399         // Upgrade success
400         connection.getIOState().onConnected();
401 
402         // Connect remote
403         remote = new WebSocketRemoteEndpoint(connection, outgoingHandler, getBatchMode());
404 
405         // Open WebSocket
406         websocket.openSession(this);
407 
408         // Open connection
409         connection.getIOState().onOpened();
410 
411         if (LOG.isDebugEnabled())
412         {
413             LOG.debug("open -> {}", dump());
414         }
415     }
416 
417     public void setExtensionFactory(ExtensionFactory extensionFactory)
418     {
419         this.extensionFactory = extensionFactory;
420     }
421 
422     /**
423      * Set the timeout in milliseconds
424      */
425     @Override
426     public void setIdleTimeout(long ms)
427     {
428         connection.setMaxIdleTimeout(ms);
429     }
430 
431     public void setOutgoingHandler(OutgoingFrames outgoing)
432     {
433         this.outgoingHandler = outgoing;
434     }
435 
436     public void setPolicy(WebSocketPolicy policy)
437     {
438         this.policy = policy;
439     }
440 
441     public void setUpgradeRequest(UpgradeRequest request)
442     {
443         this.upgradeRequest = request;
444         this.protocolVersion = request.getProtocolVersion();
445         this.parameterMap.clear();
446         if (request.getParameterMap() != null)
447         {
448             for (Map.Entry<String, List<String>> entry : request.getParameterMap().entrySet())
449             {
450                 List<String> values = entry.getValue();
451                 if (values != null)
452                 {
453                     this.parameterMap.put(entry.getKey(), values.toArray(new String[values.size()]));
454                 }
455                 else
456                 {
457                     this.parameterMap.put(entry.getKey(), new String[0]);
458                 }
459             }
460         }
461     }
462 
463     public void setUpgradeResponse(UpgradeResponse response)
464     {
465         this.upgradeResponse = response;
466     }
467 
468     @Override
469     public SuspendToken suspend()
470     {
471         return connection.suspend();
472     }
473 
474     /**
475      * @return the default (initial) value for the batching mode.
476      */
477     public BatchMode getBatchMode()
478     {
479         return BatchMode.AUTO;
480     }
481 
482     @Override
483     public String toString()
484     {
485         StringBuilder builder = new StringBuilder();
486         builder.append("WebSocketSession[");
487         builder.append("websocket=").append(websocket);
488         builder.append(",behavior=").append(policy.getBehavior());
489         builder.append(",connection=").append(connection);
490         builder.append(",remote=").append(remote);
491         builder.append(",incoming=").append(incomingHandler);
492         builder.append(",outgoing=").append(outgoingHandler);
493         builder.append("]");
494         return builder.toString();
495     }
496 }