blob: c92c58588c269ba2cae25be9d66931898f8e1c5f [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ThreadClassLoaderScope;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.CloseException;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.io.IOState;
import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
import org.eclipse.jetty.websocket.common.scopes.WebSocketSessionScope;
@ManagedObject("A Jetty WebSocket Session")
public class WebSocketSession extends ContainerLifeCycle implements Session, WebSocketSessionScope, IncomingFrames, Connection.Listener, ConnectionStateListener
{
private static final Logger LOG = Log.getLogger(WebSocketSession.class);
private static final Logger LOG_OPEN = Log.getLogger(WebSocketSession.class.getName() + "_OPEN");
private final WebSocketContainerScope containerScope;
private final URI requestURI;
private final LogicalConnection connection;
private final EventDriver websocket;
private final SessionListener[] sessionListeners;
private final Executor executor;
private final WebSocketPolicy policy;
private ClassLoader classLoader;
private ExtensionFactory extensionFactory;
private String protocolVersion;
private Map<String, String[]> parameterMap = new HashMap<>();
private WebSocketRemoteEndpoint remote;
private IncomingFrames incomingHandler;
private OutgoingFrames outgoingHandler;
private UpgradeRequest upgradeRequest;
private UpgradeResponse upgradeResponse;
private CompletableFuture<Session> openFuture;
public WebSocketSession(WebSocketContainerScope containerScope, URI requestURI, EventDriver websocket, LogicalConnection connection, SessionListener... sessionListeners)
{
Objects.requireNonNull(containerScope,"Container Scope cannot be null");
Objects.requireNonNull(requestURI,"Request URI cannot be null");
this.classLoader = Thread.currentThread().getContextClassLoader();
this.containerScope = containerScope;
this.policy = containerScope.getPolicy();
this.requestURI = requestURI;
this.websocket = websocket;
this.connection = connection;
this.sessionListeners = sessionListeners;
this.executor = connection.getExecutor();
this.outgoingHandler = connection;
this.incomingHandler = websocket;
this.connection.getIOState().addListener(this);
addBean(this.connection);
addBean(this.websocket);
}
@Override
public void close()
{
connection.close();
}
@Override
public void close(CloseStatus closeStatus)
{
this.close(closeStatus.getCode(),closeStatus.getPhrase());
}
@Override
public void close(int statusCode, String reason)
{
connection.close(statusCode,reason);
}
/**
* Harsh disconnect
*/
@Override
public void disconnect()
{
connection.disconnect();
// notify of harsh disconnect
notifyClose(StatusCode.NO_CLOSE,"Harsh disconnect");
}
public void dispatch(Runnable runnable)
{
executor.execute(runnable);
}
@Override
protected void doStart() throws Exception
{
if(LOG.isDebugEnabled())
LOG.debug("starting - {}",this);
super.doStart();
}
@Override
protected void doStop() throws Exception
{
if(LOG.isDebugEnabled())
LOG.debug("stopping - {}",this);
if (getConnection() != null)
{
try
{
getConnection().close(StatusCode.SHUTDOWN,"Shutdown");
}
catch (Throwable t)
{
LOG.debug("During Connection Shutdown",t);
}
}
super.doStop();
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
dumpThis(out);
out.append(indent).append(" +- incomingHandler : ");
if (incomingHandler instanceof Dumpable)
{
((Dumpable)incomingHandler).dump(out,indent + " ");
}
else
{
out.append(incomingHandler.toString()).append(System.lineSeparator());
}
out.append(indent).append(" +- outgoingHandler : ");
if (outgoingHandler instanceof Dumpable)
{
((Dumpable)outgoingHandler).dump(out,indent + " ");
}
else
{
out.append(outgoingHandler.toString()).append(System.lineSeparator());
}
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
WebSocketSession other = (WebSocketSession)obj;
if (connection == null)
{
if (other.connection != null)
{
return false;
}
}
else if (!connection.equals(other.connection))
{
return false;
}
return true;
}
public ByteBufferPool getBufferPool()
{
return this.connection.getBufferPool();
}
public ClassLoader getClassLoader()
{
return this.getClass().getClassLoader();
}
public LogicalConnection getConnection()
{
return connection;
}
@Override
public WebSocketContainerScope getContainerScope()
{
return this.containerScope;
}
public ExtensionFactory getExtensionFactory()
{
return extensionFactory;
}
/**
* The idle timeout in milliseconds
*/
@Override
public long getIdleTimeout()
{
return connection.getMaxIdleTimeout();
}
@ManagedAttribute(readonly = true)
public IncomingFrames getIncomingHandler()
{
return incomingHandler;
}
@Override
public InetSocketAddress getLocalAddress()
{
return connection.getLocalAddress();
}
@ManagedAttribute(readonly = true)
public OutgoingFrames getOutgoingHandler()
{
return outgoingHandler;
}
@Override
public WebSocketPolicy getPolicy()
{
return policy;
}
@Override
public String getProtocolVersion()
{
return protocolVersion;
}
@Override
public RemoteEndpoint getRemote()
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.getRemote()",policy.getBehavior(),this.getClass().getSimpleName());
ConnectionState state = connection.getIOState().getConnectionState();
if ((state == ConnectionState.OPEN) || (state == ConnectionState.CONNECTED))
{
return remote;
}
throw new WebSocketException("RemoteEndpoint unavailable, current state [" + state + "], expecting [OPEN or CONNECTED]");
}
@Override
public InetSocketAddress getRemoteAddress()
{
return remote.getInetSocketAddress();
}
public URI getRequestURI()
{
return requestURI;
}
@Override
public UpgradeRequest getUpgradeRequest()
{
return this.upgradeRequest;
}
@Override
public UpgradeResponse getUpgradeResponse()
{
return this.upgradeResponse;
}
@Override
public WebSocketSession getWebSocketSession()
{
return this;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((connection == null)?0:connection.hashCode());
return result;
}
/**
* Incoming Errors from Parser
*/
@Override
public void incomingError(Throwable t)
{
if (connection.getIOState().isInputAvailable())
{
// Forward Errors to User WebSocket Object
websocket.incomingError(t);
}
}
/**
* Incoming Raw Frames from Parser
*/
@Override
public void incomingFrame(Frame frame)
{
ClassLoader old = Thread.currentThread().getContextClassLoader();
try
{
Thread.currentThread().setContextClassLoader(classLoader);
if (connection.getIOState().isInputAvailable())
{
// Forward Frames Through Extension List
incomingHandler.incomingFrame(frame);
}
}
finally
{
Thread.currentThread().setContextClassLoader(old);
}
}
@Override
public boolean isOpen()
{
if (this.connection == null)
{
return false;
}
return this.connection.isOpen();
}
@Override
public boolean isSecure()
{
if (upgradeRequest == null)
{
throw new IllegalStateException("No valid UpgradeRequest yet");
}
URI requestURI = upgradeRequest.getRequestURI();
return "wss".equalsIgnoreCase(requestURI.getScheme());
}
public void notifyClose(int statusCode, String reason)
{
if (LOG.isDebugEnabled())
{
LOG.debug("notifyClose({},{})",statusCode,reason);
}
websocket.onClose(new CloseInfo(statusCode,reason));
}
public void notifyError(Throwable cause)
{
incomingError(cause);
}
@Override
public void onClosed(Connection connection)
{
}
@Override
public void onOpened(Connection connection)
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.onOpened()",policy.getBehavior(),this.getClass().getSimpleName());
open();
}
@SuppressWarnings("incomplete-switch")
@Override
public void onConnectionStateChange(ConnectionState state)
{
switch (state)
{
case CLOSED:
IOState ioState = this.connection.getIOState();
CloseInfo close = ioState.getCloseInfo();
// confirmed close of local endpoint
notifyClose(close.getStatusCode(),close.getReason());
// notify session listeners
for (SessionListener listener : sessionListeners)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("{}.onSessionClosed()",listener.getClass().getSimpleName());
listener.onSessionClosed(this);
}
catch (Throwable t)
{
LOG.ignore(t);
}
}
break;
case CONNECTED:
// notify session listeners
for (SessionListener listener : sessionListeners)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("{}.onSessionOpen()", listener.getClass().getSimpleName());
listener.onSessionOpened(this);
}
catch (Throwable t)
{
LOG.ignore(t);
}
}
break;
}
}
/**
* Open/Activate the session
*/
public void open()
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.open()",policy.getBehavior(),this.getClass().getSimpleName());
if (remote != null)
{
// already opened
return;
}
try(ThreadClassLoaderScope scope = new ThreadClassLoaderScope(classLoader))
{
// Upgrade success
connection.getIOState().onConnected();
// Connect remote
remote = new WebSocketRemoteEndpoint(connection,outgoingHandler,getBatchMode());
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.open() remote={}",policy.getBehavior(),this.getClass().getSimpleName(),remote);
// Open WebSocket
websocket.openSession(this);
// Open connection
connection.getIOState().onOpened();
if (LOG.isDebugEnabled())
{
LOG.debug("open -> {}",dump());
}
if(openFuture != null)
{
openFuture.complete(this);
}
}
catch (CloseException ce)
{
LOG.warn(ce);
close(ce.getStatusCode(),ce.getMessage());
}
catch (Throwable t)
{
LOG.warn(t);
// Exception on end-user WS-Endpoint.
// Fast-fail & close connection with reason.
int statusCode = StatusCode.SERVER_ERROR;
if(policy.getBehavior() == WebSocketBehavior.CLIENT)
{
statusCode = StatusCode.POLICY_VIOLATION;
}
close(statusCode,t.getMessage());
}
}
public void setExtensionFactory(ExtensionFactory extensionFactory)
{
this.extensionFactory = extensionFactory;
}
public void setFuture(CompletableFuture<Session> fut)
{
this.openFuture = fut;
}
/**
* Set the timeout in milliseconds
*/
@Override
public void setIdleTimeout(long ms)
{
connection.setMaxIdleTimeout(ms);
}
public void setOutgoingHandler(OutgoingFrames outgoing)
{
this.outgoingHandler = outgoing;
}
@Deprecated
public void setPolicy(WebSocketPolicy policy)
{
}
public void setUpgradeRequest(UpgradeRequest request)
{
this.upgradeRequest = request;
this.protocolVersion = request.getProtocolVersion();
this.parameterMap.clear();
if (request.getParameterMap() != null)
{
for (Map.Entry<String, List<String>> entry : request.getParameterMap().entrySet())
{
List<String> values = entry.getValue();
if (values != null)
{
this.parameterMap.put(entry.getKey(),values.toArray(new String[values.size()]));
}
else
{
this.parameterMap.put(entry.getKey(),new String[0]);
}
}
}
}
public void setUpgradeResponse(UpgradeResponse response)
{
this.upgradeResponse = response;
}
@Override
public SuspendToken suspend()
{
return connection.suspend();
}
/**
* @return the default (initial) value for the batching mode.
*/
public BatchMode getBatchMode()
{
return BatchMode.AUTO;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("WebSocketSession[");
builder.append("websocket=").append(websocket);
builder.append(",behavior=").append(policy.getBehavior());
builder.append(",connection=").append(connection);
builder.append(",remote=").append(remote);
builder.append(",incoming=").append(incomingHandler);
builder.append(",outgoing=").append(outgoingHandler);
builder.append("]");
return builder.toString();
}
}