You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by rz...@apache.org on 2024/03/25 10:22:03 UTC
(tomee) 01/04: TOMEE-4305 - Unpatched from Tomcat 10.0.27
This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch TOMEE-4305
in repository https://gitbox.apache.org/repos/asf/tomee.git
commit dfae75bae92ed47f10a9ceb43df2be52bc22306f
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Mon Mar 25 10:55:46 2024 +0100
TOMEE-4305 - Unpatched from Tomcat 10.0.27
---
.../org/apache/tomcat/websocket/Constants.java | 179 +++
.../org/apache/tomcat/websocket/WsSession.java | 1114 +++++++++++++++++++
.../tomcat/websocket/WsWebSocketContainer.java | 1160 ++++++++++++++++++++
.../tomcat/websocket/server/WsServerContainer.java | 597 ++++++++++
4 files changed, 3050 insertions(+)
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/Constants.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/Constants.java
new file mode 100644
index 0000000000..8dffa30399
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/Constants.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import jakarta.websocket.Extension;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ // OP Codes
+ public static final byte OPCODE_CONTINUATION = 0x00;
+ public static final byte OPCODE_TEXT = 0x01;
+ public static final byte OPCODE_BINARY = 0x02;
+ public static final byte OPCODE_CLOSE = 0x08;
+ public static final byte OPCODE_PING = 0x09;
+ public static final byte OPCODE_PONG = 0x0A;
+
+ // Internal OP Codes
+ // RFC 6455 limits OP Codes to 4 bits so these should never clash
+ // Always set bit 4 so these will be treated as control codes
+ static final byte INTERNAL_OPCODE_FLUSH = 0x18;
+
+ // Buffers
+ static final int DEFAULT_BUFFER_SIZE = Integer.getInteger(
+ "org.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE", 8 * 1024)
+ .intValue();
+
+ // Client connection
+ /**
+ * Property name to set to configure the value that is passed to
+ * {@link javax.net.ssl.SSLEngine#setEnabledProtocols(String[])}. The value
+ * should be a comma separated string.
+ */
+ public static final String SSL_PROTOCOLS_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_PROTOCOLS";
+ public static final String SSL_TRUSTSTORE_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_TRUSTSTORE";
+ public static final String SSL_TRUSTSTORE_PWD_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD";
+ public static final String SSL_TRUSTSTORE_PWD_DEFAULT = "changeit";
+ /**
+ * Property name to set to configure used SSLContext. The value should be an
+ * instance of SSLContext. If this property is present, the SSL_TRUSTSTORE*
+ * properties are ignored.
+ */
+ public static final String SSL_CONTEXT_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_CONTEXT";
+ /**
+ * Property name to set to configure the timeout (in milliseconds) when
+ * establishing a WebSocket connection to server. The default is
+ * {@link #IO_TIMEOUT_MS_DEFAULT}.
+ */
+ public static final String IO_TIMEOUT_MS_PROPERTY =
+ "org.apache.tomcat.websocket.IO_TIMEOUT_MS";
+ public static final long IO_TIMEOUT_MS_DEFAULT = 5000;
+
+ // RFC 2068 recommended a limit of 5
+ // Most browsers have a default limit of 20
+ public static final String MAX_REDIRECTIONS_PROPERTY =
+ "org.apache.tomcat.websocket.MAX_REDIRECTIONS";
+ public static final int MAX_REDIRECTIONS_DEFAULT = 20;
+
+ // HTTP upgrade header names and values
+ public static final String HOST_HEADER_NAME = "Host";
+ public static final String UPGRADE_HEADER_NAME = "Upgrade";
+ public static final String UPGRADE_HEADER_VALUE = "websocket";
+ public static final String ORIGIN_HEADER_NAME = "Origin";
+ public static final String CONNECTION_HEADER_NAME = "Connection";
+ public static final String CONNECTION_HEADER_VALUE = "upgrade";
+ public static final String LOCATION_HEADER_NAME = "Location";
+ public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
+ public static final String WWW_AUTHENTICATE_HEADER_NAME = "WWW-Authenticate";
+ public static final String PROXY_AUTHORIZATION_HEADER_NAME = "Proxy-Authorization";
+ public static final String PROXY_AUTHENTICATE_HEADER_NAME = "Proxy-Authenticate";
+ public static final String WS_VERSION_HEADER_NAME = "Sec-WebSocket-Version";
+ public static final String WS_VERSION_HEADER_VALUE = "13";
+ public static final String WS_KEY_HEADER_NAME = "Sec-WebSocket-Key";
+ public static final String WS_PROTOCOL_HEADER_NAME = "Sec-WebSocket-Protocol";
+ public static final String WS_EXTENSIONS_HEADER_NAME = "Sec-WebSocket-Extensions";
+
+ // HTTP status codes
+ public static final int MULTIPLE_CHOICES = 300;
+ public static final int MOVED_PERMANENTLY = 301;
+ public static final int FOUND = 302;
+ public static final int SEE_OTHER = 303;
+ public static final int USE_PROXY = 305;
+ public static final int TEMPORARY_REDIRECT = 307;
+ public static final int UNAUTHORIZED = 401;
+ public static final int PROXY_AUTHENTICATION_REQUIRED = 407;
+
+ // Configuration for Origin header in client
+ static final String DEFAULT_ORIGIN_HEADER_VALUE =
+ System.getProperty("org.apache.tomcat.websocket.DEFAULT_ORIGIN_HEADER_VALUE");
+
+ // Configuration for blocking sends
+ public static final String BLOCKING_SEND_TIMEOUT_PROPERTY =
+ "org.apache.tomcat.websocket.BLOCKING_SEND_TIMEOUT";
+ // Milliseconds so this is 20 seconds
+ public static final long DEFAULT_BLOCKING_SEND_TIMEOUT = 20 * 1000;
+
+ // Configuration for read idle timeout on WebSocket session
+ public static final String READ_IDLE_TIMEOUT_MS = "org.apache.tomcat.websocket.READ_IDLE_TIMEOUT_MS";
+
+ // Configuration for write idle timeout on WebSocket session
+ public static final String WRITE_IDLE_TIMEOUT_MS = "org.apache.tomcat.websocket.WRITE_IDLE_TIMEOUT_MS";
+
+ // Configuration for background processing checks intervals
+ static final int DEFAULT_PROCESS_PERIOD = Integer.getInteger(
+ "org.apache.tomcat.websocket.DEFAULT_PROCESS_PERIOD", 10)
+ .intValue();
+
+ public static final String WS_AUTHENTICATION_USER_NAME = "org.apache.tomcat.websocket.WS_AUTHENTICATION_USER_NAME";
+ public static final String WS_AUTHENTICATION_PASSWORD = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PASSWORD";
+ public static final String WS_AUTHENTICATION_REALM = "org.apache.tomcat.websocket.WS_AUTHENTICATION_REALM";
+
+ public static final String WS_AUTHENTICATION_PROXY_USER_NAME =
+ "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_USER_NAME";
+ public static final String WS_AUTHENTICATION_PROXY_PASSWORD =
+ "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_PASSWORD";
+ public static final String WS_AUTHENTICATION_PROXY_REALM =
+ "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_REALM";
+
+ /* Configuration for extensions
+ * Note: These options are primarily present to enable this implementation
+ * to pass compliance tests. They are expected to be removed once
+ * the WebSocket API includes a mechanism for adding custom extensions
+ * and disabling built-in extensions.
+ */
+ static final boolean DISABLE_BUILTIN_EXTENSIONS =
+ Boolean.getBoolean("org.apache.tomcat.websocket.DISABLE_BUILTIN_EXTENSIONS");
+ static final boolean ALLOW_UNSUPPORTED_EXTENSIONS =
+ Boolean.getBoolean("org.apache.tomcat.websocket.ALLOW_UNSUPPORTED_EXTENSIONS");
+
+ /**
+ * WebSocket specification compliance flag.
+ *
+ * @deprecated This is no longer required in Jakarta EE 10 onwards and will
+ * be removed in Tomcat 10.1.
+ */
+ @Deprecated
+ public static final boolean STRICT_SPEC_COMPLIANCE =
+ Boolean.getBoolean("org.apache.tomcat.websocket.STRICT_SPEC_COMPLIANCE");
+
+ public static final List<Extension> INSTALLED_EXTENSIONS;
+
+ static {
+ if (DISABLE_BUILTIN_EXTENSIONS) {
+ INSTALLED_EXTENSIONS = Collections.unmodifiableList(new ArrayList<>());
+ } else {
+ List<Extension> installed = new ArrayList<>(1);
+ installed.add(new WsExtension("permessage-deflate"));
+ INSTALLED_EXTENSIONS = Collections.unmodifiableList(installed);
+ }
+ }
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/WsSession.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/WsSession.java
new file mode 100644
index 0000000000..8766d46b1c
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/WsSession.java
@@ -0,0 +1,1114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.naming.NamingException;
+
+import jakarta.websocket.ClientEndpointConfig;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.CloseReason.CloseCode;
+import jakarta.websocket.CloseReason.CloseCodes;
+import jakarta.websocket.DeploymentException;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.Extension;
+import jakarta.websocket.MessageHandler;
+import jakarta.websocket.MessageHandler.Partial;
+import jakarta.websocket.MessageHandler.Whole;
+import jakarta.websocket.PongMessage;
+import jakarta.websocket.RemoteEndpoint;
+import jakarta.websocket.SendResult;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+import jakarta.websocket.server.ServerEndpointConfig;
+import jakarta.websocket.server.ServerEndpointConfig.Configurator;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.websocket.pojo.PojoEndpointServer;
+import org.apache.tomcat.websocket.server.DefaultServerEndpointConfigurator;
+
+public class WsSession implements Session {
+
+ private final Log log = LogFactory.getLog(WsSession.class); // must not be static
+ private static final StringManager sm = StringManager.getManager(WsSession.class);
+
+ // An ellipsis is a single character that looks like three periods in a row
+ // and is used to indicate a continuation.
+ private static final byte[] ELLIPSIS_BYTES = "\u2026".getBytes(StandardCharsets.UTF_8);
+ // An ellipsis is three bytes in UTF-8
+ private static final int ELLIPSIS_BYTES_LEN = ELLIPSIS_BYTES.length;
+
+ private static final boolean SEC_CONFIGURATOR_USES_IMPL_DEFAULT;
+
+ private static AtomicLong ids = new AtomicLong(0);
+
+ static {
+ // Use fake end point and path. They are never used, they just need to
+ // be sufficient to pass the validation tests.
+ ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(Object.class, "/");
+ ServerEndpointConfig sec = builder.build();
+ SEC_CONFIGURATOR_USES_IMPL_DEFAULT =
+ sec.getConfigurator().getClass().equals(DefaultServerEndpointConfigurator.class);
+ }
+
+ private final Endpoint localEndpoint;
+ private final WsRemoteEndpointImplBase wsRemoteEndpoint;
+ private final RemoteEndpoint.Async remoteEndpointAsync;
+ private final RemoteEndpoint.Basic remoteEndpointBasic;
+ private final ClassLoader applicationClassLoader;
+ private final WsWebSocketContainer webSocketContainer;
+ private final URI requestUri;
+ private final Map<String, List<String>> requestParameterMap;
+ private final String queryString;
+ private final Principal userPrincipal;
+ private final EndpointConfig endpointConfig;
+
+ private final List<Extension> negotiatedExtensions;
+ private final String subProtocol;
+ private final Map<String, String> pathParameters;
+ private final boolean secure;
+ private final String httpSessionId;
+ private final String id;
+
+ // Expected to handle message types of <String> only
+ private volatile MessageHandler textMessageHandler = null;
+ // Expected to handle message types of <ByteBuffer> only
+ private volatile MessageHandler binaryMessageHandler = null;
+ private volatile MessageHandler.Whole<PongMessage> pongMessageHandler = null;
+ private volatile State state = State.OPEN;
+ private final Object stateLock = new Object();
+ private final Map<String, Object> userProperties = new ConcurrentHashMap<>();
+ private volatile int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile long maxIdleTimeout = 0;
+ private volatile long lastActiveRead = System.currentTimeMillis();
+ private volatile long lastActiveWrite = System.currentTimeMillis();
+ private Map<FutureToSendHandler, FutureToSendHandler> futures = new ConcurrentHashMap<>();
+
+
+ /**
+ * Creates a new WebSocket session for communication between the provided
+ * client and remote end points. The result of
+ * {@link Thread#getContextClassLoader()} at the time this constructor is
+ * called will be used when calling
+ * {@link Endpoint#onClose(Session, CloseReason)}.
+ *
+ * @param clientEndpointHolder The end point managed by this code
+ * @param wsRemoteEndpoint The other / remote end point
+ * @param wsWebSocketContainer The container that created this session
+ * @param negotiatedExtensions The agreed extensions to use for this session
+ * @param subProtocol The agreed sub-protocol to use for this
+ * session
+ * @param pathParameters The path parameters associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param secure Was this session initiated over a secure
+ * connection?
+ * @param clientEndpointConfig The configuration information for the client
+ * end point
+ * @throws DeploymentException if an invalid encode is specified
+ */
+ public WsSession(ClientEndpointHolder clientEndpointHolder,
+ WsRemoteEndpointImplBase wsRemoteEndpoint,
+ WsWebSocketContainer wsWebSocketContainer,
+ List<Extension> negotiatedExtensions, String subProtocol, Map<String, String> pathParameters,
+ boolean secure, ClientEndpointConfig clientEndpointConfig) throws DeploymentException {
+ this.wsRemoteEndpoint = wsRemoteEndpoint;
+ this.wsRemoteEndpoint.setSession(this);
+ this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint);
+ this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint);
+ this.webSocketContainer = wsWebSocketContainer;
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ wsRemoteEndpoint.setSendTimeout(wsWebSocketContainer.getDefaultAsyncSendTimeout());
+ this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
+ this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();
+ this.maxIdleTimeout = webSocketContainer.getDefaultMaxSessionIdleTimeout();
+ this.requestUri = null;
+ this.requestParameterMap = Collections.emptyMap();
+ this.queryString = null;
+ this.userPrincipal = null;
+ this.httpSessionId = null;
+ this.negotiatedExtensions = negotiatedExtensions;
+ if (subProtocol == null) {
+ this.subProtocol = "";
+ } else {
+ this.subProtocol = subProtocol;
+ }
+ this.pathParameters = pathParameters;
+ this.secure = secure;
+ this.wsRemoteEndpoint.setEncoders(clientEndpointConfig);
+ this.endpointConfig = clientEndpointConfig;
+
+ this.userProperties.putAll(endpointConfig.getUserProperties());
+ this.id = Long.toHexString(ids.getAndIncrement());
+
+ this.localEndpoint = clientEndpointHolder.getInstance(getInstanceManager());
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.created", id));
+ }
+ }
+
+
+ /**
+ * Creates a new WebSocket session for communication between the provided
+ * server and remote end points. The result of
+ * {@link Thread#getContextClassLoader()} at the time this constructor is
+ * called will be used when calling
+ * {@link Endpoint#onClose(Session, CloseReason)}.
+ *
+ * @param wsRemoteEndpoint The other / remote end point
+ * @param wsWebSocketContainer The container that created this session
+ * @param requestUri The URI used to connect to this end point or
+ * <code>null</code> if this is a client session
+ * @param requestParameterMap The parameters associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param queryString The query string associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param userPrincipal The principal associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param httpSessionId The HTTP session ID associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param negotiatedExtensions The agreed extensions to use for this session
+ * @param subProtocol The agreed sub-protocol to use for this
+ * session
+ * @param pathParameters The path parameters associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param secure Was this session initiated over a secure
+ * connection?
+ * @param serverEndpointConfig The configuration information for the server
+ * end point
+ * @throws DeploymentException if an invalid encode is specified
+ */
+ public WsSession(WsRemoteEndpointImplBase wsRemoteEndpoint,
+ WsWebSocketContainer wsWebSocketContainer,
+ URI requestUri, Map<String, List<String>> requestParameterMap,
+ String queryString, Principal userPrincipal, String httpSessionId,
+ List<Extension> negotiatedExtensions, String subProtocol, Map<String, String> pathParameters,
+ boolean secure, ServerEndpointConfig serverEndpointConfig) throws DeploymentException {
+
+ this.wsRemoteEndpoint = wsRemoteEndpoint;
+ this.wsRemoteEndpoint.setSession(this);
+ this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint);
+ this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint);
+ this.webSocketContainer = wsWebSocketContainer;
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ wsRemoteEndpoint.setSendTimeout(wsWebSocketContainer.getDefaultAsyncSendTimeout());
+ this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
+ this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();
+ this.maxIdleTimeout = webSocketContainer.getDefaultMaxSessionIdleTimeout();
+ this.requestUri = requestUri;
+ if (requestParameterMap == null) {
+ this.requestParameterMap = Collections.emptyMap();
+ } else {
+ this.requestParameterMap = requestParameterMap;
+ }
+ this.queryString = queryString;
+ this.userPrincipal = userPrincipal;
+ this.httpSessionId = httpSessionId;
+ this.negotiatedExtensions = negotiatedExtensions;
+ if (subProtocol == null) {
+ this.subProtocol = "";
+ } else {
+ this.subProtocol = subProtocol;
+ }
+ this.pathParameters = pathParameters;
+ this.secure = secure;
+ this.wsRemoteEndpoint.setEncoders(serverEndpointConfig);
+ this.endpointConfig = serverEndpointConfig;
+
+ this.userProperties.putAll(endpointConfig.getUserProperties());
+ this.id = Long.toHexString(ids.getAndIncrement());
+
+ InstanceManager instanceManager = getInstanceManager();
+ Configurator configurator = serverEndpointConfig.getConfigurator();
+ Class<?> clazz = serverEndpointConfig.getEndpointClass();
+
+ Object endpointInstance;
+ try {
+ if (instanceManager == null || !isDefaultConfigurator(configurator)) {
+ endpointInstance = configurator.getEndpointInstance(clazz);
+ if (instanceManager != null) {
+ try {
+ instanceManager.newInstance(endpointInstance);
+ } catch (ReflectiveOperationException | NamingException e) {
+ throw new DeploymentException(sm.getString("wsSession.instanceNew"), e);
+ }
+ }
+ } else {
+ endpointInstance = instanceManager.newInstance(clazz);
+ }
+ } catch (ReflectiveOperationException | NamingException e) {
+ throw new DeploymentException(sm.getString("wsSession.instanceCreateFailed"), e);
+ }
+
+ if (endpointInstance instanceof Endpoint) {
+ this.localEndpoint = (Endpoint) endpointInstance;
+ } else {
+ this.localEndpoint = new PojoEndpointServer(pathParameters, endpointInstance);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.created", id));
+ }
+ }
+
+
+ private boolean isDefaultConfigurator(Configurator configurator) {
+ if (configurator.getClass().equals(DefaultServerEndpointConfigurator.class)) {
+ return true;
+ }
+ if (SEC_CONFIGURATOR_USES_IMPL_DEFAULT &&
+ configurator.getClass().equals(ServerEndpointConfig.Configurator.class)) {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Creates a new WebSocket session for communication between the two
+ * provided end points. The result of {@link Thread#getContextClassLoader()}
+ * at the time this constructor is called will be used when calling
+ * {@link Endpoint#onClose(Session, CloseReason)}.
+ *
+ * @param localEndpoint The end point managed by this code
+ * @param wsRemoteEndpoint The other / remote endpoint
+ * @param wsWebSocketContainer The container that created this session
+ * @param requestUri The URI used to connect to this endpoint or
+ * <code>null</code> is this is a client session
+ * @param requestParameterMap The parameters associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param queryString The query string associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param userPrincipal The principal associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param httpSessionId The HTTP session ID associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param negotiatedExtensions The agreed extensions to use for this session
+ * @param subProtocol The agreed subprotocol to use for this
+ * session
+ * @param pathParameters The path parameters associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param secure Was this session initiated over a secure
+ * connection?
+ * @param endpointConfig The configuration information for the
+ * endpoint
+ * @throws DeploymentException if an invalid encode is specified
+ *
+ * @deprecated Unused. This will be removed in Tomcat 10.1
+ */
+ @Deprecated
+ public WsSession(Endpoint localEndpoint,
+ WsRemoteEndpointImplBase wsRemoteEndpoint,
+ WsWebSocketContainer wsWebSocketContainer,
+ URI requestUri, Map<String, List<String>> requestParameterMap,
+ String queryString, Principal userPrincipal, String httpSessionId,
+ List<Extension> negotiatedExtensions, String subProtocol, Map<String, String> pathParameters,
+ boolean secure, EndpointConfig endpointConfig) throws DeploymentException {
+ this.localEndpoint = localEndpoint;
+ this.wsRemoteEndpoint = wsRemoteEndpoint;
+ this.wsRemoteEndpoint.setSession(this);
+ this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint);
+ this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint);
+ this.webSocketContainer = wsWebSocketContainer;
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ wsRemoteEndpoint.setSendTimeout(wsWebSocketContainer.getDefaultAsyncSendTimeout());
+ this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
+ this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();
+ this.maxIdleTimeout = webSocketContainer.getDefaultMaxSessionIdleTimeout();
+ this.requestUri = requestUri;
+ if (requestParameterMap == null) {
+ this.requestParameterMap = Collections.emptyMap();
+ } else {
+ this.requestParameterMap = requestParameterMap;
+ }
+ this.queryString = queryString;
+ this.userPrincipal = userPrincipal;
+ this.httpSessionId = httpSessionId;
+ this.negotiatedExtensions = negotiatedExtensions;
+ if (subProtocol == null) {
+ this.subProtocol = "";
+ } else {
+ this.subProtocol = subProtocol;
+ }
+ this.pathParameters = pathParameters;
+ this.secure = secure;
+ this.wsRemoteEndpoint.setEncoders(endpointConfig);
+ this.endpointConfig = endpointConfig;
+
+ this.userProperties.putAll(endpointConfig.getUserProperties());
+ this.id = Long.toHexString(ids.getAndIncrement());
+
+ InstanceManager instanceManager = getInstanceManager();
+ if (instanceManager != null) {
+ try {
+ instanceManager.newInstance(localEndpoint);
+ } catch (Exception e) {
+ throw new DeploymentException(sm.getString("wsSession.instanceNew"), e);
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.created", id));
+ }
+ }
+
+
+ public InstanceManager getInstanceManager() {
+ return webSocketContainer.getInstanceManager(applicationClassLoader);
+ }
+
+
+ @Override
+ public WebSocketContainer getContainer() {
+ checkState();
+ return webSocketContainer;
+ }
+
+
+ @Override
+ public void addMessageHandler(MessageHandler listener) {
+ Class<?> target = Util.getMessageType(listener);
+ doAddMessageHandler(target, listener);
+ }
+
+
+ @Override
+ public <T> void addMessageHandler(Class<T> clazz, Partial<T> handler)
+ throws IllegalStateException {
+ doAddMessageHandler(clazz, handler);
+ }
+
+
+ @Override
+ public <T> void addMessageHandler(Class<T> clazz, Whole<T> handler)
+ throws IllegalStateException {
+ doAddMessageHandler(clazz, handler);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private void doAddMessageHandler(Class<?> target, MessageHandler listener) {
+ checkState();
+
+ // Message handlers that require decoders may map to text messages,
+ // binary messages, both or neither.
+
+ // The frame processing code expects binary message handlers to
+ // accept ByteBuffer
+
+ // Use the POJO message handler wrappers as they are designed to wrap
+ // arbitrary objects with MessageHandlers and can wrap MessageHandlers
+ // just as easily.
+
+ Set<MessageHandlerResult> mhResults = Util.getMessageHandlers(target, listener,
+ endpointConfig, this);
+
+ for (MessageHandlerResult mhResult : mhResults) {
+ switch (mhResult.getType()) {
+ case TEXT: {
+ if (textMessageHandler != null) {
+ throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerText"));
+ }
+ textMessageHandler = mhResult.getHandler();
+ break;
+ }
+ case BINARY: {
+ if (binaryMessageHandler != null) {
+ throw new IllegalStateException(
+ sm.getString("wsSession.duplicateHandlerBinary"));
+ }
+ binaryMessageHandler = mhResult.getHandler();
+ break;
+ }
+ case PONG: {
+ if (pongMessageHandler != null) {
+ throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerPong"));
+ }
+ MessageHandler handler = mhResult.getHandler();
+ if (handler instanceof MessageHandler.Whole<?>) {
+ pongMessageHandler = (MessageHandler.Whole<PongMessage>) handler;
+ } else {
+ throw new IllegalStateException(
+ sm.getString("wsSession.invalidHandlerTypePong"));
+ }
+
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException(
+ sm.getString("wsSession.unknownHandlerType", listener, mhResult.getType()));
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public Set<MessageHandler> getMessageHandlers() {
+ checkState();
+ Set<MessageHandler> result = new HashSet<>();
+ if (binaryMessageHandler != null) {
+ result.add(binaryMessageHandler);
+ }
+ if (textMessageHandler != null) {
+ result.add(textMessageHandler);
+ }
+ if (pongMessageHandler != null) {
+ result.add(pongMessageHandler);
+ }
+ return result;
+ }
+
+
+ @Override
+ public void removeMessageHandler(MessageHandler listener) {
+ checkState();
+ if (listener == null) {
+ return;
+ }
+
+ MessageHandler wrapped = null;
+
+ if (listener instanceof WrappedMessageHandler) {
+ wrapped = ((WrappedMessageHandler) listener).getWrappedHandler();
+ }
+
+ if (wrapped == null) {
+ wrapped = listener;
+ }
+
+ boolean removed = false;
+ if (wrapped.equals(textMessageHandler) || listener.equals(textMessageHandler)) {
+ textMessageHandler = null;
+ removed = true;
+ }
+
+ if (wrapped.equals(binaryMessageHandler) || listener.equals(binaryMessageHandler)) {
+ binaryMessageHandler = null;
+ removed = true;
+ }
+
+ if (wrapped.equals(pongMessageHandler) || listener.equals(pongMessageHandler)) {
+ pongMessageHandler = null;
+ removed = true;
+ }
+
+ if (!removed) {
+ // ISE for now. Could swallow this silently / log this if the ISE
+ // becomes a problem
+ throw new IllegalStateException(
+ sm.getString("wsSession.removeHandlerFailed", listener));
+ }
+ }
+
+
+ @Override
+ public String getProtocolVersion() {
+ checkState();
+ return Constants.WS_VERSION_HEADER_VALUE;
+ }
+
+
+ @Override
+ public String getNegotiatedSubprotocol() {
+ checkState();
+ return subProtocol;
+ }
+
+
+ @Override
+ public List<Extension> getNegotiatedExtensions() {
+ checkState();
+ return negotiatedExtensions;
+ }
+
+
+ @Override
+ public boolean isSecure() {
+ checkState();
+ return secure;
+ }
+
+
+ @Override
+ public boolean isOpen() {
+ return state == State.OPEN;
+ }
+
+
+ @Override
+ public long getMaxIdleTimeout() {
+ checkState();
+ return maxIdleTimeout;
+ }
+
+
+ @Override
+ public void setMaxIdleTimeout(long timeout) {
+ checkState();
+ this.maxIdleTimeout = timeout;
+ }
+
+
+ @Override
+ public void setMaxBinaryMessageBufferSize(int max) {
+ checkState();
+ this.maxBinaryMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getMaxBinaryMessageBufferSize() {
+ checkState();
+ return maxBinaryMessageBufferSize;
+ }
+
+
+ @Override
+ public void setMaxTextMessageBufferSize(int max) {
+ checkState();
+ this.maxTextMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getMaxTextMessageBufferSize() {
+ checkState();
+ return maxTextMessageBufferSize;
+ }
+
+
+ @Override
+ public Set<Session> getOpenSessions() {
+ checkState();
+ return webSocketContainer.getOpenSessions(getSessionMapKey());
+ }
+
+
+ @Override
+ public RemoteEndpoint.Async getAsyncRemote() {
+ checkState();
+ return remoteEndpointAsync;
+ }
+
+
+ @Override
+ public RemoteEndpoint.Basic getBasicRemote() {
+ checkState();
+ return remoteEndpointBasic;
+ }
+
+
+ @Override
+ public void close() throws IOException {
+ close(new CloseReason(CloseCodes.NORMAL_CLOSURE, ""));
+ }
+
+
+ @Override
+ public void close(CloseReason closeReason) throws IOException {
+ doClose(closeReason, closeReason);
+ }
+
+
+ /**
+ * WebSocket 1.0. Section 2.1.5.
+ * Need internal close method as spec requires that the local endpoint
+ * receives a 1006 on timeout.
+ *
+ * @param closeReasonMessage The close reason to pass to the remote endpoint
+ * @param closeReasonLocal The close reason to pass to the local endpoint
+ */
+ public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal) {
+ doClose(closeReasonMessage, closeReasonLocal, false);
+ }
+
+
+ /**
+ * WebSocket 1.0. Section 2.1.5.
+ * Need internal close method as spec requires that the local endpoint
+ * receives a 1006 on timeout.
+ *
+ * @param closeReasonMessage The close reason to pass to the remote endpoint
+ * @param closeReasonLocal The close reason to pass to the local endpoint
+ * @param closeSocket Should the socket be closed immediately rather than waiting
+ * for the server to respond
+ */
+ public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal,
+ boolean closeSocket) {
+ // Double-checked locking. OK because state is volatile
+ if (state != State.OPEN) {
+ return;
+ }
+
+ synchronized (stateLock) {
+ if (state != State.OPEN) {
+ return;
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.doClose", id));
+ }
+
+ // This will trigger a flush of any batched messages.
+ try {
+ wsRemoteEndpoint.setBatchingAllowed(false);
+ } catch (IOException e) {
+ log.warn(sm.getString("wsSession.flushFailOnClose"), e);
+ fireEndpointOnError(e);
+ }
+
+ /*
+ * If the flush above fails the error handling could call this
+ * method recursively. Without this check, the close message and
+ * notifications could be sent multiple times.
+ */
+ if (state != State.OUTPUT_CLOSED) {
+ state = State.OUTPUT_CLOSED;
+
+ sendCloseMessage(closeReasonMessage);
+ if (closeSocket) {
+ wsRemoteEndpoint.close();
+ }
+ fireEndpointOnClose(closeReasonLocal);
+ }
+ }
+
+ IOException ioe = new IOException(sm.getString("wsSession.messageFailed"));
+ SendResult sr = new SendResult(ioe);
+ for (FutureToSendHandler f2sh : futures.keySet()) {
+ f2sh.onResult(sr);
+ }
+ }
+
+
+ /**
+ * Called when a close message is received. Should only ever happen once.
+ * Also called after a protocol error when the ProtocolHandler needs to
+ * force the closing of the connection.
+ *
+ * @param closeReason The reason contained within the received close
+ * message.
+ */
+ public void onClose(CloseReason closeReason) {
+
+ synchronized (stateLock) {
+ if (state != State.CLOSED) {
+ try {
+ wsRemoteEndpoint.setBatchingAllowed(false);
+ } catch (IOException e) {
+ log.warn(sm.getString("wsSession.flushFailOnClose"), e);
+ fireEndpointOnError(e);
+ }
+ if (state == State.OPEN) {
+ state = State.OUTPUT_CLOSED;
+ sendCloseMessage(closeReason);
+ fireEndpointOnClose(closeReason);
+ }
+ state = State.CLOSED;
+
+ // Close the socket
+ wsRemoteEndpoint.close();
+ }
+ }
+ }
+
+ private void fireEndpointOnClose(CloseReason closeReason) {
+
+ // Fire the onClose event
+ Throwable throwable = null;
+ InstanceManager instanceManager = getInstanceManager();
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ localEndpoint.onClose(this, closeReason);
+ } catch (Throwable t1) {
+ ExceptionUtils.handleThrowable(t1);
+ throwable = t1;
+ } finally {
+ if (instanceManager != null) {
+ try {
+ instanceManager.destroyInstance(localEndpoint);
+ } catch (Throwable t2) {
+ ExceptionUtils.handleThrowable(t2);
+ if (throwable == null) {
+ throwable = t2;
+ }
+ }
+ }
+ t.setContextClassLoader(cl);
+ }
+
+ if (throwable != null) {
+ fireEndpointOnError(throwable);
+ }
+ }
+
+
+
+ private void fireEndpointOnError(Throwable throwable) {
+
+ // Fire the onError event
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ localEndpoint.onError(this, throwable);
+ } finally {
+ t.setContextClassLoader(cl);
+ }
+ }
+
+
+ private void sendCloseMessage(CloseReason closeReason) {
+ // 125 is maximum size for the payload of a control message
+ ByteBuffer msg = ByteBuffer.allocate(125);
+ CloseCode closeCode = closeReason.getCloseCode();
+ // CLOSED_ABNORMALLY should not be put on the wire
+ if (closeCode == CloseCodes.CLOSED_ABNORMALLY) {
+ // PROTOCOL_ERROR is probably better than GOING_AWAY here
+ msg.putShort((short) CloseCodes.PROTOCOL_ERROR.getCode());
+ } else {
+ msg.putShort((short) closeCode.getCode());
+ }
+
+ String reason = closeReason.getReasonPhrase();
+ if (reason != null && reason.length() > 0) {
+ appendCloseReasonWithTruncation(msg, reason);
+ }
+ msg.flip();
+ try {
+ wsRemoteEndpoint.sendMessageBlock(Constants.OPCODE_CLOSE, msg, true);
+ } catch (IOException | IllegalStateException e) {
+ // Failed to send close message. Close the socket and let the caller
+ // deal with the Exception
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.sendCloseFail", id), e);
+ }
+ wsRemoteEndpoint.close();
+ // Failure to send a close message is not unexpected in the case of
+ // an abnormal closure (usually triggered by a failure to read/write
+ // from/to the client. In this case do not trigger the endpoint's
+ // error handling
+ if (closeCode != CloseCodes.CLOSED_ABNORMALLY) {
+ localEndpoint.onError(this, e);
+ }
+ } finally {
+ webSocketContainer.unregisterSession(getSessionMapKey(), this);
+ }
+ }
+
+
+ private Object getSessionMapKey() {
+ if (endpointConfig instanceof ServerEndpointConfig) {
+ // Server
+ return ((ServerEndpointConfig) endpointConfig).getPath();
+ } else {
+ // Client
+ return localEndpoint;
+ }
+ }
+
+ /**
+ * Use protected so unit tests can access this method directly.
+ * @param msg The message
+ * @param reason The reason
+ */
+ protected static void appendCloseReasonWithTruncation(ByteBuffer msg, String reason) {
+ // Once the close code has been added there are a maximum of 123 bytes
+ // left for the reason phrase. If it is truncated then care needs to be
+ // taken to ensure the bytes are not truncated in the middle of a
+ // multi-byte UTF-8 character.
+ byte[] reasonBytes = reason.getBytes(StandardCharsets.UTF_8);
+
+ if (reasonBytes.length <= 123) {
+ // No need to truncate
+ msg.put(reasonBytes);
+ } else {
+ // Need to truncate
+ int remaining = 123 - ELLIPSIS_BYTES_LEN;
+ int pos = 0;
+ byte[] bytesNext = reason.substring(pos, pos + 1).getBytes(StandardCharsets.UTF_8);
+ while (remaining >= bytesNext.length) {
+ msg.put(bytesNext);
+ remaining -= bytesNext.length;
+ pos++;
+ bytesNext = reason.substring(pos, pos + 1).getBytes(StandardCharsets.UTF_8);
+ }
+ msg.put(ELLIPSIS_BYTES);
+ }
+ }
+
+
+ /**
+ * Make the session aware of a {@link FutureToSendHandler} that will need to
+ * be forcibly closed if the session closes before the
+ * {@link FutureToSendHandler} completes.
+ * @param f2sh The handler
+ */
+ protected void registerFuture(FutureToSendHandler f2sh) {
+ // Ideally, this code should sync on stateLock so that the correct
+ // action is taken based on the current state of the connection.
+ // However, a sync on stateLock can't be used here as it will create the
+ // possibility of a dead-lock. See BZ 61183.
+ // Therefore, a slightly less efficient approach is used.
+
+ // Always register the future.
+ futures.put(f2sh, f2sh);
+
+ if (state == State.OPEN) {
+ // The session is open. The future has been registered with the open
+ // session. Normal processing continues.
+ return;
+ }
+
+ // The session is closed. The future may or may not have been registered
+ // in time for it to be processed during session closure.
+
+ if (f2sh.isDone()) {
+ // The future has completed. It is not known if the future was
+ // completed normally by the I/O layer or in error by doClose(). It
+ // doesn't matter which. There is nothing more to do here.
+ return;
+ }
+
+ // The session is closed. The Future had not completed when last checked.
+ // There is a small timing window that means the Future may have been
+ // completed since the last check. There is also the possibility that
+ // the Future was not registered in time to be cleaned up during session
+ // close.
+ // Attempt to complete the Future with an error result as this ensures
+ // that the Future completes and any client code waiting on it does not
+ // hang. It is slightly inefficient since the Future may have been
+ // completed in another thread or another thread may be about to
+ // complete the Future but knowing if this is the case requires the sync
+ // on stateLock (see above).
+ // Note: If multiple attempts are made to complete the Future, the
+ // second and subsequent attempts are ignored.
+
+ IOException ioe = new IOException(sm.getString("wsSession.messageFailed"));
+ SendResult sr = new SendResult(ioe);
+ f2sh.onResult(sr);
+ }
+
+
+ /**
+ * Remove a {@link FutureToSendHandler} from the set of tracked instances.
+ * @param f2sh The handler
+ */
+ protected void unregisterFuture(FutureToSendHandler f2sh) {
+ futures.remove(f2sh);
+ }
+
+
+ @Override
+ public URI getRequestURI() {
+ checkState();
+ return requestUri;
+ }
+
+
+ @Override
+ public Map<String, List<String>> getRequestParameterMap() {
+ checkState();
+ return requestParameterMap;
+ }
+
+
+ @Override
+ public String getQueryString() {
+ checkState();
+ return queryString;
+ }
+
+
+ @Override
+ public Principal getUserPrincipal() {
+ checkState();
+ return userPrincipal;
+ }
+
+
+ @Override
+ public Map<String, String> getPathParameters() {
+ checkState();
+ return pathParameters;
+ }
+
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+
+ @Override
+ public Map<String, Object> getUserProperties() {
+ checkState();
+ return userProperties;
+ }
+
+
+ public Endpoint getLocal() {
+ return localEndpoint;
+ }
+
+
+ public String getHttpSessionId() {
+ return httpSessionId;
+ }
+
+
+ protected MessageHandler getTextMessageHandler() {
+ return textMessageHandler;
+ }
+
+
+ protected MessageHandler getBinaryMessageHandler() {
+ return binaryMessageHandler;
+ }
+
+
+ protected MessageHandler.Whole<PongMessage> getPongMessageHandler() {
+ return pongMessageHandler;
+ }
+
+
+ protected void updateLastActiveRead() {
+ lastActiveRead = System.currentTimeMillis();
+ }
+
+
+ protected void updateLastActiveWrite() {
+ lastActiveWrite = System.currentTimeMillis();
+ }
+
+
+ protected void checkExpiration() {
+ // Local copies to ensure consistent behaviour during method execution
+ long timeout = maxIdleTimeout;
+ long timeoutRead = getMaxIdleTimeoutRead();
+ long timeoutWrite = getMaxIdleTimeoutWrite();
+
+ long currentTime = System.currentTimeMillis();
+ String key = null;
+
+ if (timeoutRead > 0 && (currentTime - lastActiveRead) > timeoutRead) {
+ key = "wsSession.timeoutRead";
+ } else if (timeoutWrite > 0 && (currentTime - lastActiveWrite) > timeoutWrite) {
+ key = "wsSession.timeoutWrite";
+ } else if (timeout > 0 && (currentTime - lastActiveRead) > timeout &&
+ (currentTime - lastActiveWrite) > timeout) {
+ key = "wsSession.timeout";
+ }
+
+ if (key != null) {
+ String msg = sm.getString(key, getId());
+ if (log.isDebugEnabled()) {
+ log.debug(msg);
+ }
+ doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg));
+ }
+ }
+
+
+ private long getMaxIdleTimeoutRead() {
+ Object timeout = userProperties.get(Constants.READ_IDLE_TIMEOUT_MS);
+ if (timeout instanceof Long) {
+ return ((Long) timeout).longValue();
+ }
+ return 0;
+ }
+
+
+ private long getMaxIdleTimeoutWrite() {
+ Object timeout = userProperties.get(Constants.WRITE_IDLE_TIMEOUT_MS);
+ if (timeout instanceof Long) {
+ return ((Long) timeout).longValue();
+ }
+ return 0;
+ }
+
+
+ private void checkState() {
+ if (state == State.CLOSED) {
+ /*
+ * As per RFC 6455, a WebSocket connection is considered to be
+ * closed once a peer has sent and received a WebSocket close frame.
+ */
+ throw new IllegalStateException(sm.getString("wsSession.closed", id));
+ }
+ }
+
+ private enum State {
+ OPEN,
+ OUTPUT_CLOSED,
+ CLOSED
+ }
+
+
+ private WsFrameBase wsFrame;
+ void setWsFrame(WsFrameBase wsFrame) {
+ this.wsFrame = wsFrame;
+ }
+
+
+ /**
+ * Suspends the reading of the incoming messages.
+ */
+ public void suspend() {
+ wsFrame.suspend();
+ }
+
+
+ /**
+ * Resumes the reading of the incoming messages.
+ */
+ public void resume() {
+ wsFrame.resume();
+ }
+}
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/WsWebSocketContainer.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
new file mode 100644
index 0000000000..509fabe979
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
@@ -0,0 +1,1160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousChannelGroup;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManagerFactory;
+
+import jakarta.websocket.ClientEndpoint;
+import jakarta.websocket.ClientEndpointConfig;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.CloseReason.CloseCodes;
+import jakarta.websocket.DeploymentException;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.Extension;
+import jakarta.websocket.HandshakeResponse;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.InstanceManagerBindings;
+import org.apache.tomcat.util.buf.StringUtils;
+import org.apache.tomcat.util.codec.binary.Base64;
+import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
+import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.util.security.KeyStoreUtil;
+
+public class WsWebSocketContainer implements WebSocketContainer, BackgroundProcess {
+
+ private static final StringManager sm = StringManager.getManager(WsWebSocketContainer.class);
+ private static final Random RANDOM = new Random();
+ private static final byte[] CRLF = new byte[] { 13, 10 };
+
+ private static final byte[] GET_BYTES = "GET ".getBytes(StandardCharsets.ISO_8859_1);
+ private static final byte[] ROOT_URI_BYTES = "/".getBytes(StandardCharsets.ISO_8859_1);
+ private static final byte[] HTTP_VERSION_BYTES =
+ " HTTP/1.1\r\n".getBytes(StandardCharsets.ISO_8859_1);
+
+ private volatile AsynchronousChannelGroup asynchronousChannelGroup = null;
+ private final Object asynchronousChannelGroupLock = new Object();
+
+ private final Log log = LogFactory.getLog(WsWebSocketContainer.class); // must not be static
+ // Server side uses the endpoint path as the key
+ // Client side uses the client endpoint instance
+ private final Map<Object, Set<WsSession>> endpointSessionMap = new HashMap<>();
+ private final Map<WsSession,WsSession> sessions = new ConcurrentHashMap<>();
+ private final Object endPointSessionMapLock = new Object();
+
+ private long defaultAsyncTimeout = -1;
+ private int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile long defaultMaxSessionIdleTimeout = 0;
+ private int backgroundProcessCount = 0;
+ private int processPeriod = Constants.DEFAULT_PROCESS_PERIOD;
+
+ private InstanceManager instanceManager;
+
+ protected InstanceManager getInstanceManager(ClassLoader classLoader) {
+ if (instanceManager != null) {
+ return instanceManager;
+ }
+ return InstanceManagerBindings.get(classLoader);
+ }
+
+ protected void setInstanceManager(InstanceManager instanceManager) {
+ this.instanceManager = instanceManager;
+ }
+
+ @Override
+ public Session connectToServer(Object pojo, URI path) throws DeploymentException {
+ ClientEndpointConfig config = createClientEndpointConfig(pojo.getClass());
+ ClientEndpointHolder holder = new PojoHolder(pojo, config);
+ return connectToServerRecursive(holder, config, path, new HashSet<>());
+ }
+
+
+ @Override
+ public Session connectToServer(Class<?> annotatedEndpointClass, URI path) throws DeploymentException {
+ ClientEndpointConfig config = createClientEndpointConfig(annotatedEndpointClass);
+ ClientEndpointHolder holder = new PojoClassHolder(annotatedEndpointClass, config);
+ return connectToServerRecursive(holder, config, path, new HashSet<>());
+ }
+
+
+ private ClientEndpointConfig createClientEndpointConfig(Class<?> annotatedEndpointClass)
+ throws DeploymentException {
+ ClientEndpoint annotation = annotatedEndpointClass.getAnnotation(ClientEndpoint.class);
+ if (annotation == null) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.missingAnnotation",
+ annotatedEndpointClass.getName()));
+ }
+
+ Class<? extends ClientEndpointConfig.Configurator> configuratorClazz =
+ annotation.configurator();
+
+ ClientEndpointConfig.Configurator configurator = null;
+ if (!ClientEndpointConfig.Configurator.class.equals(
+ configuratorClazz)) {
+ try {
+ configurator = configuratorClazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.defaultConfiguratorFail"), e);
+ }
+ }
+
+ ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create();
+ // Avoid NPE when using RI API JAR - see BZ 56343
+ if (configurator != null) {
+ builder.configurator(configurator);
+ }
+ ClientEndpointConfig config = builder.
+ decoders(Arrays.asList(annotation.decoders())).
+ encoders(Arrays.asList(annotation.encoders())).
+ preferredSubprotocols(Arrays.asList(annotation.subprotocols())).
+ build();
+
+ return config;
+ }
+
+
+ @Override
+ public Session connectToServer(Class<? extends Endpoint> clazz, ClientEndpointConfig clientEndpointConfiguration,
+ URI path) throws DeploymentException {
+ ClientEndpointHolder holder = new EndpointClassHolder(clazz);
+ return connectToServerRecursive(holder, clientEndpointConfiguration, path, new HashSet<>());
+ }
+
+
+ @Override
+ public Session connectToServer(Endpoint endpoint, ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException {
+ ClientEndpointHolder holder = new EndpointHolder(endpoint);
+ return connectToServerRecursive(holder, clientEndpointConfiguration, path, new HashSet<>());
+ }
+
+
+ private Session connectToServerRecursive(ClientEndpointHolder clientEndpointHolder,
+ ClientEndpointConfig clientEndpointConfiguration, URI path, Set<URI> redirectSet)
+ throws DeploymentException {
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsWebSocketContainer.connect.entry", clientEndpointHolder.getClassName(), path));
+ }
+
+ boolean secure = false;
+ ByteBuffer proxyConnect = null;
+ URI proxyPath;
+
+ // Validate scheme (and build proxyPath)
+ String scheme = path.getScheme();
+ if ("ws".equalsIgnoreCase(scheme)) {
+ proxyPath = URI.create("http" + path.toString().substring(2));
+ } else if ("wss".equalsIgnoreCase(scheme)) {
+ proxyPath = URI.create("https" + path.toString().substring(3));
+ secure = true;
+ } else {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.pathWrongScheme", scheme));
+ }
+
+ // Validate host
+ String host = path.getHost();
+ if (host == null) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.pathNoHost"));
+ }
+ int port = path.getPort();
+
+ SocketAddress sa = null;
+
+ // Check to see if a proxy is configured. Javadoc indicates return value
+ // will never be null
+ List<Proxy> proxies = ProxySelector.getDefault().select(proxyPath);
+ Proxy selectedProxy = null;
+ for (Proxy proxy : proxies) {
+ if (proxy.type().equals(Proxy.Type.HTTP)) {
+ sa = proxy.address();
+ if (sa instanceof InetSocketAddress) {
+ InetSocketAddress inet = (InetSocketAddress) sa;
+ if (inet.isUnresolved()) {
+ sa = new InetSocketAddress(inet.getHostName(), inet.getPort());
+ }
+ }
+ selectedProxy = proxy;
+ break;
+ }
+ }
+
+ // If the port is not explicitly specified, compute it based on the
+ // scheme
+ if (port == -1) {
+ if ("ws".equalsIgnoreCase(scheme)) {
+ port = 80;
+ } else {
+ // Must be wss due to scheme validation above
+ port = 443;
+ }
+ }
+
+ Map<String,Object> userProperties = clientEndpointConfiguration.getUserProperties();
+
+ // If sa is null, no proxy is configured so need to create sa
+ if (sa == null) {
+ sa = new InetSocketAddress(host, port);
+ } else {
+ proxyConnect = createProxyRequest(
+ host, port, (String) userProperties.get(Constants.PROXY_AUTHORIZATION_HEADER_NAME));
+ }
+
+ // Create the initial HTTP request to open the WebSocket connection
+ Map<String, List<String>> reqHeaders = createRequestHeaders(host, port, secure,
+ clientEndpointConfiguration);
+ clientEndpointConfiguration.getConfigurator().beforeRequest(reqHeaders);
+ if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null
+ && !reqHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) {
+ List<String> originValues = new ArrayList<>(1);
+ originValues.add(Constants.DEFAULT_ORIGIN_HEADER_VALUE);
+ reqHeaders.put(Constants.ORIGIN_HEADER_NAME, originValues);
+ }
+ ByteBuffer request = createRequest(path, reqHeaders);
+
+ AsynchronousSocketChannel socketChannel;
+ try {
+ socketChannel = AsynchronousSocketChannel.open(getAsynchronousChannelGroup());
+ } catch (IOException ioe) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.asynchronousSocketChannelFail"), ioe);
+ }
+
+ // Get the connection timeout
+ long timeout = Constants.IO_TIMEOUT_MS_DEFAULT;
+ String timeoutValue = (String) userProperties.get(Constants.IO_TIMEOUT_MS_PROPERTY);
+ if (timeoutValue != null) {
+ timeout = Long.valueOf(timeoutValue).intValue();
+ }
+
+ // Set-up
+ // Same size as the WsFrame input buffer
+ ByteBuffer response = ByteBuffer.allocate(getDefaultMaxBinaryMessageBufferSize());
+ String subProtocol;
+ boolean success = false;
+ List<Extension> extensionsAgreed = new ArrayList<>();
+ Transformation transformation = null;
+ AsyncChannelWrapper channel = null;
+
+ try {
+ // Open the connection
+ Future<Void> fConnect = socketChannel.connect(sa);
+
+ if (proxyConnect != null) {
+ fConnect.get(timeout, TimeUnit.MILLISECONDS);
+ // Proxy CONNECT is clear text
+ channel = new AsyncChannelWrapperNonSecure(socketChannel);
+ writeRequest(channel, proxyConnect, timeout);
+ HttpResponse httpResponse = processResponse(response, channel, timeout);
+ if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) {
+ return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path,
+ redirectSet, userProperties, request, httpResponse, AuthenticationType.PROXY);
+ } else if (httpResponse.getStatus() != 200) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.proxyConnectFail", selectedProxy,
+ Integer.toString(httpResponse.getStatus())));
+ }
+ }
+
+ if (secure) {
+ // Regardless of whether a non-secure wrapper was created for a
+ // proxy CONNECT, need to use TLS from this point on so wrap the
+ // original AsynchronousSocketChannel
+ SSLEngine sslEngine = createSSLEngine(userProperties, host, port);
+ channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine);
+ } else if (channel == null) {
+ // Only need to wrap as this point if it wasn't wrapped to process a
+ // proxy CONNECT
+ channel = new AsyncChannelWrapperNonSecure(socketChannel);
+ }
+
+ fConnect.get(timeout, TimeUnit.MILLISECONDS);
+
+ Future<Void> fHandshake = channel.handshake();
+ fHandshake.get(timeout, TimeUnit.MILLISECONDS);
+
+ if (log.isDebugEnabled()) {
+ SocketAddress localAddress = null;
+ try {
+ localAddress = channel.getLocalAddress();
+ } catch (IOException ioe) {
+ // Ignore
+ }
+ log.debug(sm.getString("wsWebSocketContainer.connect.write",
+ Integer.valueOf(request.position()), Integer.valueOf(request.limit()), localAddress));
+ }
+ writeRequest(channel, request, timeout);
+
+ HttpResponse httpResponse = processResponse(response, channel, timeout);
+
+ // Check maximum permitted redirects
+ int maxRedirects = Constants.MAX_REDIRECTIONS_DEFAULT;
+ String maxRedirectsValue =
+ (String) userProperties.get(Constants.MAX_REDIRECTIONS_PROPERTY);
+ if (maxRedirectsValue != null) {
+ maxRedirects = Integer.parseInt(maxRedirectsValue);
+ }
+
+ if (httpResponse.status != 101) {
+ if(isRedirectStatus(httpResponse.status)){
+ List<String> locationHeader =
+ httpResponse.getHandshakeResponse().getHeaders().get(
+ Constants.LOCATION_HEADER_NAME);
+
+ if (locationHeader == null || locationHeader.isEmpty() ||
+ locationHeader.get(0) == null || locationHeader.get(0).isEmpty()) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.missingLocationHeader",
+ Integer.toString(httpResponse.status)));
+ }
+
+ URI redirectLocation = URI.create(locationHeader.get(0)).normalize();
+
+ if (!redirectLocation.isAbsolute()) {
+ redirectLocation = path.resolve(redirectLocation);
+ }
+
+ String redirectScheme = redirectLocation.getScheme().toLowerCase();
+
+ if (redirectScheme.startsWith("http")) {
+ redirectLocation = new URI(redirectScheme.replace("http", "ws"),
+ redirectLocation.getUserInfo(), redirectLocation.getHost(),
+ redirectLocation.getPort(), redirectLocation.getPath(),
+ redirectLocation.getQuery(), redirectLocation.getFragment());
+ }
+
+ if (!redirectSet.add(redirectLocation) || redirectSet.size() > maxRedirects) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.redirectThreshold", redirectLocation,
+ Integer.toString(redirectSet.size()),
+ Integer.toString(maxRedirects)));
+ }
+
+ return connectToServerRecursive(
+ clientEndpointHolder, clientEndpointConfiguration, redirectLocation, redirectSet);
+
+ } else if (httpResponse.status == Constants.UNAUTHORIZED) {
+ return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path,
+ redirectSet, userProperties, request, httpResponse, AuthenticationType.WWW);
+
+ } else {
+ throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus",
+ Integer.toString(httpResponse.status)));
+ }
+ }
+ HandshakeResponse handshakeResponse = httpResponse.getHandshakeResponse();
+ clientEndpointConfiguration.getConfigurator().afterResponse(handshakeResponse);
+
+ // Sub-protocol
+ List<String> protocolHeaders = handshakeResponse.getHeaders().get(
+ Constants.WS_PROTOCOL_HEADER_NAME);
+ if (protocolHeaders == null || protocolHeaders.size() == 0) {
+ subProtocol = null;
+ } else if (protocolHeaders.size() == 1) {
+ subProtocol = protocolHeaders.get(0);
+ } else {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.invalidSubProtocol"));
+ }
+
+ // Extensions
+ // Should normally only be one header but handle the case of
+ // multiple headers
+ List<String> extHeaders = handshakeResponse.getHeaders().get(
+ Constants.WS_EXTENSIONS_HEADER_NAME);
+ if (extHeaders != null) {
+ for (String extHeader : extHeaders) {
+ Util.parseExtensionHeader(extensionsAgreed, extHeader);
+ }
+ }
+
+ // Build the transformations
+ TransformationFactory factory = TransformationFactory.getInstance();
+ for (Extension extension : extensionsAgreed) {
+ List<List<Extension.Parameter>> wrapper = new ArrayList<>(1);
+ wrapper.add(extension.getParameters());
+ Transformation t = factory.create(extension.getName(), wrapper, false);
+ if (t == null) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.invalidExtensionParameters"));
+ }
+ if (transformation == null) {
+ transformation = t;
+ } else {
+ transformation.setNext(t);
+ }
+ }
+
+ success = true;
+ } catch (ExecutionException | InterruptedException | SSLException |
+ EOFException | TimeoutException | URISyntaxException | AuthenticationException e) {
+ throw new DeploymentException(sm.getString("wsWebSocketContainer.httpRequestFailed", path), e);
+ } finally {
+ if (!success) {
+ if (channel != null) {
+ channel.close();
+ } else {
+ try {
+ socketChannel.close();
+ } catch (IOException ioe) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+ // Switch to WebSocket
+ WsRemoteEndpointImplClient wsRemoteEndpointClient = new WsRemoteEndpointImplClient(channel);
+
+ WsSession wsSession = new WsSession(clientEndpointHolder, wsRemoteEndpointClient, this, extensionsAgreed,
+ subProtocol, Collections.<String,String>emptyMap(), secure, clientEndpointConfiguration);
+
+ WsFrameClient wsFrameClient = new WsFrameClient(response, channel,
+ wsSession, transformation);
+ // WsFrame adds the necessary final transformations. Copy the
+ // completed transformation chain to the remote end point.
+ wsRemoteEndpointClient.setTransformation(wsFrameClient.getTransformation());
+
+ wsSession.getLocal().onOpen(wsSession, clientEndpointConfiguration);
+ registerSession(wsSession.getLocal(), wsSession);
+
+ /* It is possible that the server sent one or more messages as soon as
+ * the WebSocket connection was established. Depending on the exact
+ * timing of when those messages were sent they could be sat in the
+ * input buffer waiting to be read and will not trigger a "data
+ * available to read" event. Therefore, it is necessary to process the
+ * input buffer here. Note that this happens on the current thread which
+ * means that this thread will be used for any onMessage notifications.
+ * This is a special case. Subsequent "data available to read" events
+ * will be handled by threads from the AsyncChannelGroup's executor.
+ */
+ wsFrameClient.startInputProcessing();
+
+ return wsSession;
+ }
+
+
+ private Session processAuthenticationChallenge(ClientEndpointHolder clientEndpointHolder,
+ ClientEndpointConfig clientEndpointConfiguration, URI path, Set<URI> redirectSet,
+ Map<String,Object> userProperties, ByteBuffer request, HttpResponse httpResponse,
+ AuthenticationType authenticationType)
+ throws DeploymentException, AuthenticationException {
+
+ if (userProperties.get(authenticationType.getAuthorizationHeaderName()) != null) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.failedAuthentication",
+ Integer.valueOf(httpResponse.status), authenticationType.getAuthorizationHeaderName()));
+ }
+
+ List<String> authenticateHeaders = httpResponse.getHandshakeResponse().getHeaders().get(
+ authenticationType.getAuthenticateHeaderName());
+
+ if (authenticateHeaders == null || authenticateHeaders.isEmpty() ||
+ authenticateHeaders.get(0) == null || authenticateHeaders.get(0).isEmpty()) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.missingAuthenticateHeader",
+ Integer.toString(httpResponse.status), authenticationType.getAuthenticateHeaderName()));
+ }
+
+ String authScheme = authenticateHeaders.get(0).split("\\s+", 2)[0];
+
+ Authenticator auth = AuthenticatorFactory.getAuthenticator(authScheme);
+
+ if (auth == null) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.unsupportedAuthScheme",
+ Integer.valueOf(httpResponse.status), authScheme));
+ }
+
+ String requestUri = new String(request.array(), StandardCharsets.ISO_8859_1).split("\\s", 3)[1];
+
+ userProperties.put(authenticationType.getAuthorizationHeaderName(), auth.getAuthorization(
+ requestUri, authenticateHeaders.get(0),
+ (String) userProperties.get(authenticationType.getUserNameProperty()),
+ (String) userProperties.get(authenticationType.getUserPasswordProperty()),
+ (String) userProperties.get(authenticationType.getUserRealmProperty())));
+
+ return connectToServerRecursive(
+ clientEndpointHolder, clientEndpointConfiguration, path, redirectSet);
+ }
+
+
+
+ private static void writeRequest(AsyncChannelWrapper channel, ByteBuffer request,
+ long timeout) throws TimeoutException, InterruptedException, ExecutionException {
+ int toWrite = request.limit();
+
+ Future<Integer> fWrite = channel.write(request);
+ Integer thisWrite = fWrite.get(timeout, TimeUnit.MILLISECONDS);
+ toWrite -= thisWrite.intValue();
+
+ while (toWrite > 0) {
+ fWrite = channel.write(request);
+ thisWrite = fWrite.get(timeout, TimeUnit.MILLISECONDS);
+ toWrite -= thisWrite.intValue();
+ }
+ }
+
+
+ private static boolean isRedirectStatus(int httpResponseCode) {
+
+ boolean isRedirect = false;
+
+ switch (httpResponseCode) {
+ case Constants.MULTIPLE_CHOICES:
+ case Constants.MOVED_PERMANENTLY:
+ case Constants.FOUND:
+ case Constants.SEE_OTHER:
+ case Constants.USE_PROXY:
+ case Constants.TEMPORARY_REDIRECT:
+ isRedirect = true;
+ break;
+ default:
+ break;
+ }
+
+ return isRedirect;
+ }
+
+
+ private static ByteBuffer createProxyRequest(String host, int port, String authorizationHeader) {
+ StringBuilder request = new StringBuilder();
+ request.append("CONNECT ");
+ request.append(host);
+ request.append(':');
+ request.append(port);
+
+ request.append(" HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keepalive\r\nHost: ");
+ request.append(host);
+ request.append(':');
+ request.append(port);
+
+ if (authorizationHeader != null) {
+ request.append("\r\n");
+ request.append(Constants.PROXY_AUTHORIZATION_HEADER_NAME);
+ request.append(':');
+ request.append(authorizationHeader);
+ }
+
+ request.append("\r\n\r\n");
+
+ byte[] bytes = request.toString().getBytes(StandardCharsets.ISO_8859_1);
+ return ByteBuffer.wrap(bytes);
+ }
+
+ protected void registerSession(Object key, WsSession wsSession) {
+
+ if (!wsSession.isOpen()) {
+ // The session was closed during onOpen. No need to register it.
+ return;
+ }
+ synchronized (endPointSessionMapLock) {
+ if (endpointSessionMap.size() == 0) {
+ BackgroundProcessManager.getInstance().register(this);
+ }
+ Set<WsSession> wsSessions = endpointSessionMap.get(key);
+ if (wsSessions == null) {
+ wsSessions = new HashSet<>();
+ endpointSessionMap.put(key, wsSessions);
+ }
+ wsSessions.add(wsSession);
+ }
+ sessions.put(wsSession, wsSession);
+ }
+
+
+ protected void unregisterSession(Object key, WsSession wsSession) {
+
+ synchronized (endPointSessionMapLock) {
+ Set<WsSession> wsSessions = endpointSessionMap.get(key);
+ if (wsSessions != null) {
+ wsSessions.remove(wsSession);
+ if (wsSessions.size() == 0) {
+ endpointSessionMap.remove(key);
+ }
+ }
+ if (endpointSessionMap.size() == 0) {
+ BackgroundProcessManager.getInstance().unregister(this);
+ }
+ }
+ sessions.remove(wsSession);
+ }
+
+
+ Set<Session> getOpenSessions(Object key) {
+ HashSet<Session> result = new HashSet<>();
+ synchronized (endPointSessionMapLock) {
+ Set<WsSession> sessions = endpointSessionMap.get(key);
+ if (sessions != null) {
+ result.addAll(sessions);
+ }
+ }
+ return result;
+ }
+
+ private static Map<String, List<String>> createRequestHeaders(String host, int port,
+ boolean secure, ClientEndpointConfig clientEndpointConfiguration) {
+
+ Map<String, List<String>> headers = new HashMap<>();
+ List<Extension> extensions = clientEndpointConfiguration.getExtensions();
+ List<String> subProtocols = clientEndpointConfiguration.getPreferredSubprotocols();
+ Map<String, Object> userProperties = clientEndpointConfiguration.getUserProperties();
+
+ if (userProperties.get(Constants.AUTHORIZATION_HEADER_NAME) != null) {
+ List<String> authValues = new ArrayList<>(1);
+ authValues.add((String) userProperties.get(Constants.AUTHORIZATION_HEADER_NAME));
+ headers.put(Constants.AUTHORIZATION_HEADER_NAME, authValues);
+ }
+
+ // Host header
+ List<String> hostValues = new ArrayList<>(1);
+ if (port == 80 && !secure || port == 443 && secure) {
+ // Default ports. Do not include port in host header
+ hostValues.add(host);
+ } else {
+ hostValues.add(host + ':' + port);
+ }
+
+ headers.put(Constants.HOST_HEADER_NAME, hostValues);
+
+ // Upgrade header
+ List<String> upgradeValues = new ArrayList<>(1);
+ upgradeValues.add(Constants.UPGRADE_HEADER_VALUE);
+ headers.put(Constants.UPGRADE_HEADER_NAME, upgradeValues);
+
+ // Connection header
+ List<String> connectionValues = new ArrayList<>(1);
+ connectionValues.add(Constants.CONNECTION_HEADER_VALUE);
+ headers.put(Constants.CONNECTION_HEADER_NAME, connectionValues);
+
+ // WebSocket version header
+ List<String> wsVersionValues = new ArrayList<>(1);
+ wsVersionValues.add(Constants.WS_VERSION_HEADER_VALUE);
+ headers.put(Constants.WS_VERSION_HEADER_NAME, wsVersionValues);
+
+ // WebSocket key
+ List<String> wsKeyValues = new ArrayList<>(1);
+ wsKeyValues.add(generateWsKeyValue());
+ headers.put(Constants.WS_KEY_HEADER_NAME, wsKeyValues);
+
+ // WebSocket sub-protocols
+ if (subProtocols != null && subProtocols.size() > 0) {
+ headers.put(Constants.WS_PROTOCOL_HEADER_NAME, subProtocols);
+ }
+
+ // WebSocket extensions
+ if (extensions != null && extensions.size() > 0) {
+ headers.put(Constants.WS_EXTENSIONS_HEADER_NAME,
+ generateExtensionHeaders(extensions));
+ }
+
+ return headers;
+ }
+
+
+ private static List<String> generateExtensionHeaders(List<Extension> extensions) {
+ List<String> result = new ArrayList<>(extensions.size());
+ for (Extension extension : extensions) {
+ StringBuilder header = new StringBuilder();
+ header.append(extension.getName());
+ for (Extension.Parameter param : extension.getParameters()) {
+ header.append(';');
+ header.append(param.getName());
+ String value = param.getValue();
+ if (value != null && value.length() > 0) {
+ header.append('=');
+ header.append(value);
+ }
+ }
+ result.add(header.toString());
+ }
+ return result;
+ }
+
+
+ private static String generateWsKeyValue() {
+ byte[] keyBytes = new byte[16];
+ RANDOM.nextBytes(keyBytes);
+ return Base64.encodeBase64String(keyBytes);
+ }
+
+
+ private static ByteBuffer createRequest(URI uri, Map<String,List<String>> reqHeaders) {
+ ByteBuffer result = ByteBuffer.allocate(4 * 1024);
+
+ // Request line
+ result.put(GET_BYTES);
+ final String path = uri.getPath();
+ if (null == path || path.isEmpty()) {
+ result.put(ROOT_URI_BYTES);
+ } else {
+ result.put(uri.getRawPath().getBytes(StandardCharsets.ISO_8859_1));
+ }
+ String query = uri.getRawQuery();
+ if (query != null) {
+ result.put((byte) '?');
+ result.put(query.getBytes(StandardCharsets.ISO_8859_1));
+ }
+ result.put(HTTP_VERSION_BYTES);
+
+ // Headers
+ for (Entry<String, List<String>> entry : reqHeaders.entrySet()) {
+ result = addHeader(result, entry.getKey(), entry.getValue());
+ }
+
+ // Terminating CRLF
+ result.put(CRLF);
+
+ result.flip();
+
+ return result;
+ }
+
+
+ private static ByteBuffer addHeader(ByteBuffer result, String key, List<String> values) {
+ if (values.isEmpty()) {
+ return result;
+ }
+
+ result = putWithExpand(result, key.getBytes(StandardCharsets.ISO_8859_1));
+ result = putWithExpand(result, ": ".getBytes(StandardCharsets.ISO_8859_1));
+ result = putWithExpand(result, StringUtils.join(values).getBytes(StandardCharsets.ISO_8859_1));
+ result = putWithExpand(result, CRLF);
+
+ return result;
+ }
+
+
+ private static ByteBuffer putWithExpand(ByteBuffer input, byte[] bytes) {
+ if (bytes.length > input.remaining()) {
+ int newSize;
+ if (bytes.length > input.capacity()) {
+ newSize = 2 * bytes.length;
+ } else {
+ newSize = input.capacity() * 2;
+ }
+ ByteBuffer expanded = ByteBuffer.allocate(newSize);
+ input.flip();
+ expanded.put(input);
+ input = expanded;
+ }
+ return input.put(bytes);
+ }
+
+
+ /**
+ * Process response, blocking until HTTP response has been fully received.
+ * @throws ExecutionException if there is an exception reading the response
+ * @throws InterruptedException if the thread is interrupted while reading
+ * the response
+ * @throws DeploymentException if the response status line is not correctly
+ * formatted
+ * @throws TimeoutException if the response was not read within the expected
+ * timeout
+ */
+ private HttpResponse processResponse(ByteBuffer response,
+ AsyncChannelWrapper channel, long timeout) throws InterruptedException,
+ ExecutionException, DeploymentException, EOFException,
+ TimeoutException {
+
+ Map<String,List<String>> headers = new CaseInsensitiveKeyMap<>();
+
+ int status = 0;
+ boolean readStatus = false;
+ boolean readHeaders = false;
+ String line = null;
+ while (!readHeaders) {
+ // On entering loop buffer will be empty and at the start of a new
+ // loop the buffer will have been fully read.
+ response.clear();
+ // Blocking read
+ Future<Integer> read = channel.read(response);
+ Integer bytesRead;
+ try {
+ bytesRead = read.get(timeout, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ TimeoutException te = new TimeoutException(
+ sm.getString("wsWebSocketContainer.responseFail", Integer.toString(status), headers));
+ te.initCause(e);
+ throw te;
+ }
+ if (bytesRead.intValue() == -1) {
+ throw new EOFException(sm.getString("wsWebSocketContainer.responseFail", Integer.toString(status), headers));
+ }
+ response.flip();
+ while (response.hasRemaining() && !readHeaders) {
+ if (line == null) {
+ line = readLine(response);
+ } else {
+ line += readLine(response);
+ }
+ if ("\r\n".equals(line)) {
+ readHeaders = true;
+ } else if (line.endsWith("\r\n")) {
+ if (readStatus) {
+ parseHeaders(line, headers);
+ } else {
+ status = parseStatus(line);
+ readStatus = true;
+ }
+ line = null;
+ }
+ }
+ }
+
+ return new HttpResponse(status, new WsHandshakeResponse(headers));
+ }
+
+
+ private int parseStatus(String line) throws DeploymentException {
+ // This client only understands HTTP 1.
+ // RFC2616 is case specific
+ String[] parts = line.trim().split(" ");
+ // CONNECT for proxy may return a 1.0 response
+ if (parts.length < 2 || !("HTTP/1.0".equals(parts[0]) || "HTTP/1.1".equals(parts[0]))) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.invalidStatus", line));
+ }
+ try {
+ return Integer.parseInt(parts[1]);
+ } catch (NumberFormatException nfe) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.invalidStatus", line));
+ }
+ }
+
+
+ private void parseHeaders(String line, Map<String,List<String>> headers) {
+ // Treat headers as single values by default.
+
+ int index = line.indexOf(':');
+ if (index == -1) {
+ log.warn(sm.getString("wsWebSocketContainer.invalidHeader", line));
+ return;
+ }
+ // Header names are case insensitive so always use lower case
+ String headerName = line.substring(0, index).trim().toLowerCase(Locale.ENGLISH);
+ // Multi-value headers are stored as a single header and the client is
+ // expected to handle splitting into individual values
+ String headerValue = line.substring(index + 1).trim();
+
+ List<String> values = headers.get(headerName);
+ if (values == null) {
+ values = new ArrayList<>(1);
+ headers.put(headerName, values);
+ }
+ values.add(headerValue);
+ }
+
+ private String readLine(ByteBuffer response) {
+ // All ISO-8859-1
+ StringBuilder sb = new StringBuilder();
+
+ char c = 0;
+ while (response.hasRemaining()) {
+ c = (char) response.get();
+ sb.append(c);
+ if (c == 10) {
+ break;
+ }
+ }
+
+ return sb.toString();
+ }
+
+
+ private SSLEngine createSSLEngine(Map<String,Object> userProperties, String host, int port)
+ throws DeploymentException {
+
+ try {
+ // See if a custom SSLContext has been provided
+ SSLContext sslContext =
+ (SSLContext) userProperties.get(Constants.SSL_CONTEXT_PROPERTY);
+
+ if (sslContext == null) {
+ // Create the SSL Context
+ sslContext = SSLContext.getInstance("TLS");
+
+ // Trust store
+ String sslTrustStoreValue =
+ (String) userProperties.get(Constants.SSL_TRUSTSTORE_PROPERTY);
+ if (sslTrustStoreValue != null) {
+ String sslTrustStorePwdValue = (String) userProperties.get(
+ Constants.SSL_TRUSTSTORE_PWD_PROPERTY);
+ if (sslTrustStorePwdValue == null) {
+ sslTrustStorePwdValue = Constants.SSL_TRUSTSTORE_PWD_DEFAULT;
+ }
+
+ File keyStoreFile = new File(sslTrustStoreValue);
+ KeyStore ks = KeyStore.getInstance("JKS");
+ try (InputStream is = new FileInputStream(keyStoreFile)) {
+ KeyStoreUtil.load(ks, is, sslTrustStorePwdValue.toCharArray());
+ }
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ } else {
+ sslContext.init(null, null, null);
+ }
+ }
+
+ SSLEngine engine = sslContext.createSSLEngine(host, port);
+
+ String sslProtocolsValue =
+ (String) userProperties.get(Constants.SSL_PROTOCOLS_PROPERTY);
+ if (sslProtocolsValue != null) {
+ engine.setEnabledProtocols(sslProtocolsValue.split(","));
+ }
+
+ engine.setUseClientMode(true);
+
+ // Enable host verification
+ // Start with current settings (returns a copy)
+ SSLParameters sslParams = engine.getSSLParameters();
+ // Use HTTPS since WebSocket starts over HTTP(S)
+ sslParams.setEndpointIdentificationAlgorithm("HTTPS");
+ // Write the parameters back
+ engine.setSSLParameters(sslParams);
+
+ return engine;
+ } catch (Exception e) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.sslEngineFail"), e);
+ }
+ }
+
+
+ @Override
+ public long getDefaultMaxSessionIdleTimeout() {
+ return defaultMaxSessionIdleTimeout;
+ }
+
+
+ @Override
+ public void setDefaultMaxSessionIdleTimeout(long timeout) {
+ this.defaultMaxSessionIdleTimeout = timeout;
+ }
+
+
+ @Override
+ public int getDefaultMaxBinaryMessageBufferSize() {
+ return maxBinaryMessageBufferSize;
+ }
+
+
+ @Override
+ public void setDefaultMaxBinaryMessageBufferSize(int max) {
+ maxBinaryMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getDefaultMaxTextMessageBufferSize() {
+ return maxTextMessageBufferSize;
+ }
+
+
+ @Override
+ public void setDefaultMaxTextMessageBufferSize(int max) {
+ maxTextMessageBufferSize = max;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Currently, this implementation does not support any extensions.
+ */
+ @Override
+ public Set<Extension> getInstalledExtensions() {
+ return Collections.emptySet();
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value for this implementation is -1.
+ */
+ @Override
+ public long getDefaultAsyncSendTimeout() {
+ return defaultAsyncTimeout;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value for this implementation is -1.
+ */
+ @Override
+ public void setAsyncSendTimeout(long timeout) {
+ this.defaultAsyncTimeout = timeout;
+ }
+
+
+ /**
+ * Cleans up the resources still in use by WebSocket sessions created from
+ * this container. This includes closing sessions and cancelling
+ * {@link Future}s associated with blocking read/writes.
+ */
+ public void destroy() {
+ CloseReason cr = new CloseReason(
+ CloseCodes.GOING_AWAY, sm.getString("wsWebSocketContainer.shutdown"));
+
+ for (WsSession session : sessions.keySet()) {
+ try {
+ session.close(cr);
+ } catch (IOException ioe) {
+ log.debug(sm.getString(
+ "wsWebSocketContainer.sessionCloseFail", session.getId()), ioe);
+ }
+ }
+
+ // Only unregister with AsyncChannelGroupUtil if this instance
+ // registered with it
+ if (asynchronousChannelGroup != null) {
+ synchronized (asynchronousChannelGroupLock) {
+ if (asynchronousChannelGroup != null) {
+ AsyncChannelGroupUtil.unregister();
+ asynchronousChannelGroup = null;
+ }
+ }
+ }
+ }
+
+
+ private AsynchronousChannelGroup getAsynchronousChannelGroup() {
+ // Use AsyncChannelGroupUtil to share a common group amongst all
+ // WebSocket clients
+ AsynchronousChannelGroup result = asynchronousChannelGroup;
+ if (result == null) {
+ synchronized (asynchronousChannelGroupLock) {
+ if (asynchronousChannelGroup == null) {
+ asynchronousChannelGroup = AsyncChannelGroupUtil.register();
+ }
+ result = asynchronousChannelGroup;
+ }
+ }
+ return result;
+ }
+
+
+ // ----------------------------------------------- BackgroundProcess methods
+
+ @Override
+ public void backgroundProcess() {
+ // This method gets called once a second.
+ backgroundProcessCount ++;
+ if (backgroundProcessCount >= processPeriod) {
+ backgroundProcessCount = 0;
+
+ for (WsSession wsSession : sessions.keySet()) {
+ wsSession.checkExpiration();
+ }
+ }
+
+ }
+
+
+ @Override
+ public void setProcessPeriod(int period) {
+ this.processPeriod = period;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value is 10 which means session expirations are processed
+ * every 10 seconds.
+ */
+ @Override
+ public int getProcessPeriod() {
+ return processPeriod;
+ }
+
+
+ private static class HttpResponse {
+ private final int status;
+ private final HandshakeResponse handshakeResponse;
+
+ public HttpResponse(int status, HandshakeResponse handshakeResponse) {
+ this.status = status;
+ this.handshakeResponse = handshakeResponse;
+ }
+
+
+ public int getStatus() {
+ return status;
+ }
+
+
+ public HandshakeResponse getHandshakeResponse() {
+ return handshakeResponse;
+ }
+ }
+}
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/server/WsServerContainer.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/server/WsServerContainer.java
new file mode 100644
index 0000000000..6d2171989a
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/websocket/server/WsServerContainer.java
@@ -0,0 +1,597 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import javax.naming.NamingException;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.FilterRegistration;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.CloseReason.CloseCodes;
+import jakarta.websocket.DeploymentException;
+import jakarta.websocket.Encoder;
+import jakarta.websocket.server.ServerContainer;
+import jakarta.websocket.server.ServerEndpoint;
+import jakarta.websocket.server.ServerEndpointConfig;
+import jakarta.websocket.server.ServerEndpointConfig.Configurator;
+
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.websocket.WsSession;
+import org.apache.tomcat.websocket.WsWebSocketContainer;
+import org.apache.tomcat.websocket.pojo.PojoMethodMapping;
+
+/**
+ * Provides a per class loader (i.e. per web application) instance of a
+ * ServerContainer. Web application wide defaults may be configured by setting
+ * the following servlet context initialisation parameters to the desired
+ * values.
+ * <ul>
+ * <li>{@link Constants#BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}</li>
+ * <li>{@link Constants#TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}</li>
+ * </ul>
+ */
+public class WsServerContainer extends WsWebSocketContainer
+ implements ServerContainer {
+
+ private static final StringManager sm = StringManager.getManager(WsServerContainer.class);
+
+ private static final CloseReason AUTHENTICATED_HTTP_SESSION_CLOSED =
+ new CloseReason(CloseCodes.VIOLATED_POLICY,
+ "This connection was established under an authenticated " +
+ "HTTP session that has ended.");
+
+ private final WsWriteTimeout wsWriteTimeout = new WsWriteTimeout();
+
+ private final ServletContext servletContext;
+ private final Map<String,ExactPathMatch> configExactMatchMap = new ConcurrentHashMap<>();
+ private final Map<Integer,ConcurrentSkipListMap<String,TemplatePathMatch>> configTemplateMatchMap =
+ new ConcurrentHashMap<>();
+ @SuppressWarnings("deprecation")
+ private volatile boolean enforceNoAddAfterHandshake =
+ org.apache.tomcat.websocket.Constants.STRICT_SPEC_COMPLIANCE;
+ private volatile boolean addAllowed = true;
+ private final Map<String,Set<WsSession>> authenticatedSessions = new ConcurrentHashMap<>();
+ private volatile boolean endpointsRegistered = false;
+ private volatile boolean deploymentFailed = false;
+
+ @SuppressWarnings("deprecation")
+ WsServerContainer(ServletContext servletContext) {
+
+ this.servletContext = servletContext;
+ setInstanceManager((InstanceManager) servletContext.getAttribute(InstanceManager.class.getName()));
+
+ // Configure servlet context wide defaults
+ String value = servletContext.getInitParameter(
+ Constants.BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM);
+ if (value != null) {
+ setDefaultMaxBinaryMessageBufferSize(Integer.parseInt(value));
+ }
+
+ value = servletContext.getInitParameter(
+ Constants.TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM);
+ if (value != null) {
+ setDefaultMaxTextMessageBufferSize(Integer.parseInt(value));
+ }
+
+ value = servletContext.getInitParameter(
+ Constants.ENFORCE_NO_ADD_AFTER_HANDSHAKE_CONTEXT_INIT_PARAM);
+ if (value != null) {
+ setEnforceNoAddAfterHandshake(Boolean.parseBoolean(value));
+ }
+
+ FilterRegistration.Dynamic fr = servletContext.addFilter(
+ "Tomcat WebSocket (JSR356) Filter", new WsFilter());
+ fr.setAsyncSupported(true);
+
+ EnumSet<DispatcherType> types = EnumSet.of(DispatcherType.REQUEST,
+ DispatcherType.FORWARD);
+
+ fr.addMappingForUrlPatterns(types, true, "/*");
+ }
+
+
+ /**
+ * Published the provided endpoint implementation at the specified path with
+ * the specified configuration. {@link #WsServerContainer(ServletContext)}
+ * must be called before calling this method.
+ *
+ * @param sec The configuration to use when creating endpoint instances
+ * @throws DeploymentException if the endpoint cannot be published as
+ * requested
+ */
+ @Override
+ public void addEndpoint(ServerEndpointConfig sec) throws DeploymentException {
+ addEndpoint(sec, false);
+ }
+
+
+ void addEndpoint(ServerEndpointConfig sec, boolean fromAnnotatedPojo) throws DeploymentException {
+
+ if (enforceNoAddAfterHandshake && !addAllowed) {
+ throw new DeploymentException(
+ sm.getString("serverContainer.addNotAllowed"));
+ }
+
+ if (servletContext == null) {
+ throw new DeploymentException(
+ sm.getString("serverContainer.servletContextMissing"));
+ }
+
+ if (deploymentFailed) {
+ throw new DeploymentException(sm.getString("serverContainer.failedDeployment",
+ servletContext.getContextPath(), servletContext.getVirtualServerName()));
+ }
+
+ try {
+ String path = sec.getPath();
+
+ // Add method mapping to user properties
+ PojoMethodMapping methodMapping = new PojoMethodMapping(sec.getEndpointClass(),
+ sec.getDecoders(), path, getInstanceManager(Thread.currentThread().getContextClassLoader()));
+ if (methodMapping.getOnClose() != null || methodMapping.getOnOpen() != null
+ || methodMapping.getOnError() != null || methodMapping.hasMessageHandlers()) {
+ sec.getUserProperties().put(org.apache.tomcat.websocket.pojo.Constants.POJO_METHOD_MAPPING_KEY,
+ methodMapping);
+ }
+
+ UriTemplate uriTemplate = new UriTemplate(path);
+ if (uriTemplate.hasParameters()) {
+ Integer key = Integer.valueOf(uriTemplate.getSegmentCount());
+ ConcurrentSkipListMap<String,TemplatePathMatch> templateMatches =
+ configTemplateMatchMap.get(key);
+ if (templateMatches == null) {
+ // Ensure that if concurrent threads execute this block they
+ // all end up using the same ConcurrentSkipListMap instance
+ templateMatches = new ConcurrentSkipListMap<>();
+ configTemplateMatchMap.putIfAbsent(key, templateMatches);
+ templateMatches = configTemplateMatchMap.get(key);
+ }
+ TemplatePathMatch newMatch = new TemplatePathMatch(sec, uriTemplate, fromAnnotatedPojo);
+ TemplatePathMatch oldMatch = templateMatches.putIfAbsent(uriTemplate.getNormalizedPath(), newMatch);
+ if (oldMatch != null) {
+ // Note: This depends on Endpoint instances being added
+ // before POJOs in WsSci#onStartup()
+ if (oldMatch.isFromAnnotatedPojo() && !newMatch.isFromAnnotatedPojo() &&
+ oldMatch.getConfig().getEndpointClass() == newMatch.getConfig().getEndpointClass()) {
+ // The WebSocket spec says to ignore the new match in this case
+ templateMatches.put(path, oldMatch);
+ } else {
+ // Duplicate uriTemplate;
+ throw new DeploymentException(
+ sm.getString("serverContainer.duplicatePaths", path,
+ sec.getEndpointClass(),
+ sec.getEndpointClass()));
+ }
+ }
+ } else {
+ // Exact match
+ ExactPathMatch newMatch = new ExactPathMatch(sec, fromAnnotatedPojo);
+ ExactPathMatch oldMatch = configExactMatchMap.put(path, newMatch);
+ if (oldMatch != null) {
+ // Note: This depends on Endpoint instances being added
+ // before POJOs in WsSci#onStartup()
+ if (oldMatch.isFromAnnotatedPojo() && !newMatch.isFromAnnotatedPojo() &&
+ oldMatch.getConfig().getEndpointClass() == newMatch.getConfig().getEndpointClass()) {
+ // The WebSocket spec says to ignore the new match in this case
+ configExactMatchMap.put(path, oldMatch);
+ } else {
+ // Duplicate path mappings
+ throw new DeploymentException(
+ sm.getString("serverContainer.duplicatePaths", path,
+ oldMatch.getConfig().getEndpointClass(),
+ sec.getEndpointClass()));
+ }
+ }
+ }
+
+ endpointsRegistered = true;
+ } catch (DeploymentException de) {
+ failDeployment();
+ throw de;
+ }
+ }
+
+
+ /**
+ * Provides the equivalent of {@link #addEndpoint(ServerEndpointConfig)}
+ * for publishing plain old java objects (POJOs) that have been annotated as
+ * WebSocket endpoints.
+ *
+ * @param pojo The annotated POJO
+ */
+ @Override
+ public void addEndpoint(Class<?> pojo) throws DeploymentException {
+ addEndpoint(pojo, false);
+ }
+
+
+ void addEndpoint(Class<?> pojo, boolean fromAnnotatedPojo) throws DeploymentException {
+
+ if (deploymentFailed) {
+ throw new DeploymentException(sm.getString("serverContainer.failedDeployment",
+ servletContext.getContextPath(), servletContext.getVirtualServerName()));
+ }
+
+ ServerEndpointConfig sec;
+
+ try {
+ ServerEndpoint annotation = pojo.getAnnotation(ServerEndpoint.class);
+ if (annotation == null) {
+ throw new DeploymentException(
+ sm.getString("serverContainer.missingAnnotation",
+ pojo.getName()));
+ }
+ String path = annotation.value();
+
+ // Validate encoders
+ validateEncoders(annotation.encoders(), getInstanceManager(Thread.currentThread().getContextClassLoader()));
+
+ // ServerEndpointConfig
+ Class<? extends Configurator> configuratorClazz =
+ annotation.configurator();
+ Configurator configurator = null;
+ if (!configuratorClazz.equals(Configurator.class)) {
+ try {
+ configurator = annotation.configurator().getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(sm.getString(
+ "serverContainer.configuratorFail",
+ annotation.configurator().getName(),
+ pojo.getClass().getName()), e);
+ }
+ }
+ sec = ServerEndpointConfig.Builder.create(pojo, path).
+ decoders(Arrays.asList(annotation.decoders())).
+ encoders(Arrays.asList(annotation.encoders())).
+ subprotocols(Arrays.asList(annotation.subprotocols())).
+ configurator(configurator).
+ build();
+ } catch (DeploymentException de) {
+ failDeployment();
+ throw de;
+ }
+
+ addEndpoint(sec, fromAnnotatedPojo);
+ }
+
+
+ void failDeployment() {
+ deploymentFailed = true;
+
+ // Clear all existing deployments
+ endpointsRegistered = false;
+ configExactMatchMap.clear();
+ configTemplateMatchMap.clear();
+ }
+
+
+ boolean areEndpointsRegistered() {
+ return endpointsRegistered;
+ }
+
+
+ /**
+ * Until the WebSocket specification provides such a mechanism, this Tomcat
+ * proprietary method is provided to enable applications to programmatically
+ * determine whether or not to upgrade an individual request to WebSocket.
+ * <p>
+ * Note: This method is not used by Tomcat but is used directly by
+ * third-party code and must not be removed.
+ *
+ * @param request The request object to be upgraded
+ * @param response The response object to be populated with the result of
+ * the upgrade
+ * @param sec The server endpoint to use to process the upgrade request
+ * @param pathParams The path parameters associated with the upgrade request
+ *
+ * @throws ServletException If a configuration error prevents the upgrade
+ * from taking place
+ * @throws IOException If an I/O error occurs during the upgrade process
+ *
+ * @deprecated This method will be removed in Apache Tomcat 10.1 onwards. It
+ * has been replaced by {@link #upgradeHttpToWebSocket(Object,
+ * Object, ServerEndpointConfig, Map)}
+ */
+ @Deprecated
+ public void doUpgrade(HttpServletRequest request,
+ HttpServletResponse response, ServerEndpointConfig sec,
+ Map<String,String> pathParams)
+ throws ServletException, IOException {
+ UpgradeUtil.doUpgrade(this, request, response, sec, pathParams);
+ }
+
+
+ /**
+ * Upgrade the HTTP connection represented by the {@code HttpServletRequest} and {@code HttpServletResponse} to the
+ * WebSocket protocol and establish a WebSocket connection as per the provided {@link ServerEndpointConfig}.
+ * <p>
+ * This method is primarily intended to be used by frameworks that implement the front-controller pattern. It does
+ * not deploy the provided endpoint.
+ * <p>
+ * If the WebSocket implementation is not deployed as part of a Jakarta Servlet container, this method will throw an
+ * {@link UnsupportedOperationException}.
+ * <p>
+ * This method will be part of the Jakarta WebSocket API from version 2.1
+ *
+ * @param httpServletRequest The {@code HttpServletRequest} to be processed as a WebSocket handshake as per
+ * section 4.0 of RFC 6455.
+ * @param httpServletResponse The {@code HttpServletResponse} to be used when processing the
+ * {@code httpServletRequest} as a WebSocket handshake as per section 4.0 of RFC 6455.
+ * @param sec The server endpoint configuration to use to configure the WebSocket endpoint
+ * @param pathParameters Provides a mapping of path parameter names and values, if any, to be used for the
+ * WebSocket connection established by the call to this method. If no such mapping is
+ * defined, an empty Map must be passed.
+ *
+ * @throws IllegalStateException if the provided request does not meet the requirements of the WebSocket handshake
+ * @throws UnsupportedOperationException if the WebSocket implementation is not deployed as part of a Jakarta
+ * Servlet container
+ * @throws IOException if an I/O error occurs during the establishment of a WebSocket connection
+ * @throws DeploymentException if a configuration error prevents the establishment of a WebSocket connection
+ */
+ public void upgradeHttpToWebSocket(Object httpServletRequest, Object httpServletResponse,
+ ServerEndpointConfig sec, Map<String, String> pathParameters)
+ throws IOException, DeploymentException {
+ try {
+ UpgradeUtil.doUpgrade(this, (HttpServletRequest) httpServletRequest, (HttpServletResponse) httpServletResponse,
+ sec, pathParameters);
+ } catch (ServletException e) {
+ throw new DeploymentException(e.getMessage(), e);
+ }
+ }
+
+
+ public WsMappingResult findMapping(String path) {
+
+ // Prevent registering additional endpoints once the first attempt has
+ // been made to use one
+ if (addAllowed) {
+ addAllowed = false;
+ }
+
+ // Check an exact match. Simple case as there are no templates.
+ ExactPathMatch match = configExactMatchMap.get(path);
+ if (match != null) {
+ return new WsMappingResult(match.getConfig(), Collections.<String, String>emptyMap());
+ }
+
+ // No exact match. Need to look for template matches.
+ UriTemplate pathUriTemplate = null;
+ try {
+ pathUriTemplate = new UriTemplate(path);
+ } catch (DeploymentException e) {
+ // Path is not valid so can't be matched to a WebSocketEndpoint
+ return null;
+ }
+
+ // Number of segments has to match
+ Integer key = Integer.valueOf(pathUriTemplate.getSegmentCount());
+ ConcurrentSkipListMap<String,TemplatePathMatch> templateMatches = configTemplateMatchMap.get(key);
+
+ if (templateMatches == null) {
+ // No templates with an equal number of segments so there will be
+ // no matches
+ return null;
+ }
+
+ // List is in alphabetical order of normalised templates.
+ // Correct match is the first one that matches.
+ ServerEndpointConfig sec = null;
+ Map<String,String> pathParams = null;
+ for (TemplatePathMatch templateMatch : templateMatches.values()) {
+ pathParams = templateMatch.getUriTemplate().match(pathUriTemplate);
+ if (pathParams != null) {
+ sec = templateMatch.getConfig();
+ break;
+ }
+ }
+
+ if (sec == null) {
+ // No match
+ return null;
+ }
+
+ return new WsMappingResult(sec, pathParams);
+ }
+
+
+ @Deprecated
+ public boolean isEnforceNoAddAfterHandshake() {
+ return enforceNoAddAfterHandshake;
+ }
+
+
+ @Deprecated
+ public void setEnforceNoAddAfterHandshake(
+ boolean enforceNoAddAfterHandshake) {
+ this.enforceNoAddAfterHandshake = enforceNoAddAfterHandshake;
+ }
+
+
+ protected WsWriteTimeout getTimeout() {
+ return wsWriteTimeout;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overridden to make it visible to other classes in this package.
+ */
+ @Override
+ protected InstanceManager getInstanceManager(ClassLoader classLoader) {
+ return super.getInstanceManager(classLoader);
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overridden to make it visible to other classes in this package.
+ */
+ @Override
+ protected void registerSession(Object key, WsSession wsSession) {
+ super.registerSession(key, wsSession);
+ if (wsSession.isOpen() &&
+ wsSession.getUserPrincipal() != null &&
+ wsSession.getHttpSessionId() != null) {
+ registerAuthenticatedSession(wsSession,
+ wsSession.getHttpSessionId());
+ }
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overridden to make it visible to other classes in this package.
+ */
+ @Override
+ protected void unregisterSession(Object key, WsSession wsSession) {
+ if (wsSession.getUserPrincipal() != null &&
+ wsSession.getHttpSessionId() != null) {
+ unregisterAuthenticatedSession(wsSession,
+ wsSession.getHttpSessionId());
+ }
+ super.unregisterSession(key, wsSession);
+ }
+
+
+ private void registerAuthenticatedSession(WsSession wsSession,
+ String httpSessionId) {
+ Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
+ if (wsSessions == null) {
+ wsSessions = Collections.newSetFromMap(
+ new ConcurrentHashMap<>());
+ authenticatedSessions.putIfAbsent(httpSessionId, wsSessions);
+ wsSessions = authenticatedSessions.get(httpSessionId);
+ }
+ wsSessions.add(wsSession);
+ }
+
+
+ private void unregisterAuthenticatedSession(WsSession wsSession,
+ String httpSessionId) {
+ Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
+ // wsSessions will be null if the HTTP session has ended
+ if (wsSessions != null) {
+ wsSessions.remove(wsSession);
+ }
+ }
+
+
+ public void closeAuthenticatedSession(String httpSessionId) {
+ Set<WsSession> wsSessions = authenticatedSessions.remove(httpSessionId);
+
+ if (wsSessions != null && !wsSessions.isEmpty()) {
+ for (WsSession wsSession : wsSessions) {
+ try {
+ wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
+ } catch (IOException e) {
+ // Any IOExceptions during close will have been caught and the
+ // onError method called.
+ }
+ }
+ }
+ }
+
+
+ private static void validateEncoders(Class<? extends Encoder>[] encoders, InstanceManager instanceManager)
+ throws DeploymentException {
+
+ for (Class<? extends Encoder> encoder : encoders) {
+ // Need to instantiate encoder to ensure it is valid and that
+ // deployment can be failed if it is not. The encoder is then
+ // discarded immediately.
+ Encoder instance;
+ try {
+ if (instanceManager == null) {
+ instance = encoder.getConstructor().newInstance();
+ } else {
+ instance = (Encoder) instanceManager.newInstance(encoder);
+ instanceManager.destroyInstance(instance);
+ }
+ } catch(ReflectiveOperationException | NamingException e) {
+ throw new DeploymentException(sm.getString(
+ "serverContainer.encoderFail", encoder.getName()), e);
+ }
+ }
+ }
+
+
+ private static class TemplatePathMatch {
+ private final ServerEndpointConfig config;
+ private final UriTemplate uriTemplate;
+ private final boolean fromAnnotatedPojo;
+
+ public TemplatePathMatch(ServerEndpointConfig config, UriTemplate uriTemplate,
+ boolean fromAnnotatedPojo) {
+ this.config = config;
+ this.uriTemplate = uriTemplate;
+ this.fromAnnotatedPojo = fromAnnotatedPojo;
+ }
+
+
+ public ServerEndpointConfig getConfig() {
+ return config;
+ }
+
+
+ public UriTemplate getUriTemplate() {
+ return uriTemplate;
+ }
+
+
+ public boolean isFromAnnotatedPojo() {
+ return fromAnnotatedPojo;
+ }
+ }
+
+
+ private static class ExactPathMatch {
+ private final ServerEndpointConfig config;
+ private final boolean fromAnnotatedPojo;
+
+ public ExactPathMatch(ServerEndpointConfig config, boolean fromAnnotatedPojo) {
+ this.config = config;
+ this.fromAnnotatedPojo = fromAnnotatedPojo;
+ }
+
+
+ public ServerEndpointConfig getConfig() {
+ return config;
+ }
+
+
+ public boolean isFromAnnotatedPojo() {
+ return fromAnnotatedPojo;
+ }
+ }
+}