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;
+        }
+    }
+}