| // |
| // ======================================================================== |
| // 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(); |
| } |
| |
| } |