This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 92a428c944 Handle HTTP sessionID changes for WsSession close on HTTP 
session expiry
92a428c944 is described below

commit 92a428c9445212b6e64896ddc4339b40cb3a8e31
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Aug 7 12:38:32 2026 +0100

    Handle HTTP sessionID changes for WsSession close on HTTP session expiry
---
 .../apache/catalina/ha/session/DeltaSession.java   |  74 ++++++-----
 java/org/apache/tomcat/websocket/WsSession.java    |  14 +-
 .../server/WsHttpSessionBindingListener.java       |  47 +++++++
 .../websocket/server/WsHttpUpgradeHandler.java     |  16 +--
 java/org/apache/tomcat/websocket/server/WsSci.java |   1 -
 .../tomcat/websocket/server/WsServerContainer.java | 121 +++++++++++++-----
 .../tomcat/websocket/server/WsSessionListener.java |  31 -----
 .../tomcat/websocket/TestWebSocketFrameClient.java | 142 ++++++++++++++++++++-
 webapps/docs/changelog.xml                         |   5 +
 9 files changed, 341 insertions(+), 110 deletions(-)

diff --git a/java/org/apache/catalina/ha/session/DeltaSession.java 
b/java/org/apache/catalina/ha/session/DeltaSession.java
index 4a517de988..7508d5ce26 100644
--- a/java/org/apache/catalina/ha/session/DeltaSession.java
+++ b/java/org/apache/catalina/ha/session/DeltaSession.java
@@ -116,11 +116,11 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     }
 
     /**
-     * Create a DeltaRequest instance. This protected method enables 
subclasses to override and use
-     * custom DeltaRequest implementations.
+     * Create a DeltaRequest instance. This protected method enables 
subclasses to override and use custom DeltaRequest
+     * implementations.
      *
-     * @param sessionId         Session identifier
-     * @param recordAllActions  Record all actions, including duplicates
+     * @param sessionId        Session identifier
+     * @param recordAllActions Record all actions, including duplicates
      *
      * @return New DeltaRequest instance
      */
@@ -311,8 +311,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Set the maximum inactive interval.
      *
-     * @param interval          Max inactive interval in seconds
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param interval        Max inactive interval in seconds
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setMaxInactiveInterval(int interval, boolean addDeltaRequest) {
         super.maxInactiveInterval = interval;
@@ -334,8 +334,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Set the new flag.
      *
-     * @param isNew             New flag value
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param isNew           New flag value
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setNew(boolean isNew, boolean addDeltaRequest) {
         super.setNew(isNew);
@@ -357,8 +357,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Set the session principal.
      *
-     * @param principal         Session principal
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param principal       Session principal
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setPrincipal(Principal principal, boolean addDeltaRequest) {
         lockInternal();
@@ -380,8 +380,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Set the authentication type.
      *
-     * @param authType          Authentication type
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param authType        Authentication type
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setAuthType(String authType, boolean addDeltaRequest) {
         lockInternal();
@@ -444,8 +444,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Expire this session.
      *
-     * @param notify          Whether to notify session listeners
-     * @param notifyCluster   Whether to notify the cluster of expiration
+     * @param notify        Whether to notify session listeners
+     * @param notifyCluster Whether to notify the cluster of expiration
      */
     public void expire(boolean notify, boolean notifyCluster) {
 
@@ -516,8 +516,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Add a session listener.
      *
-     * @param listener          Session listener to add
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param listener        Session listener to add
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void addSessionListener(SessionListener listener, boolean 
addDeltaRequest) {
         lockInternal();
@@ -539,8 +539,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Remove a session listener.
      *
-     * @param listener          Session listener to remove
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param listener        Session listener to remove
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void removeSessionListener(SessionListener listener, boolean 
addDeltaRequest) {
         lockInternal();
@@ -683,9 +683,9 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Remove an attribute from this session.
      *
-     * @param name              Attribute name
-     * @param notify            Whether to notify listeners
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Attribute name
+     * @param notify          Whether to notify listeners
+     * @param addDeltaRequest Whether to add a delta request entry
      *
      * @throws IllegalStateException If this session is no longer valid
      */
@@ -705,10 +705,10 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Set an attribute on this session.
      *
-     * @param name              Attribute name
-     * @param value             Attribute value
-     * @param notify            Whether to notify listeners
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Attribute name
+     * @param value           Attribute value
+     * @param notify          Whether to notify listeners
+     * @param addDeltaRequest Whether to add a delta request entry
      *
      * @throws IllegalArgumentException If name is null
      */
@@ -728,7 +728,13 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
         lockInternal();
         try {
             super.setAttribute(name, value, notify);
-            if (addDeltaRequest && !exclude(name, value)) {
+            /*
+             * It is possible that the session expires concurrently with the 
attribute being added. Depending on the
+             * exact timing, one of two things will happen. Either an 
IllegalStateException will be thrown or the
+             * attribute will be added and then immediately removed from the 
session. The exception will be re-thrown.
+             * If the attribute is removed, don't update the deltaRequest.
+             */
+            if (getAttribute(name) != null && addDeltaRequest && 
!exclude(name, value)) {
                 deltaRequest.setAttribute(name, value);
             }
         } finally {
@@ -745,8 +751,8 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Remove a note from this session.
      *
-     * @param name              Note name
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Note name
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void removeNote(String name, boolean addDeltaRequest) {
         lockInternal();
@@ -769,9 +775,9 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Set a note on this session.
      *
-     * @param name              Note name
-     * @param value             Note value
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Note name
+     * @param value           Note value
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setNote(String name, Object value, boolean addDeltaRequest) {
 
@@ -971,9 +977,9 @@ public class DeltaSession extends StandardSession 
implements Externalizable, Clu
     /**
      * Remove an attribute from this session without additional validation.
      *
-     * @param name              Attribute name
-     * @param notify            Whether to notify listeners
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Attribute name
+     * @param notify          Whether to notify listeners
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     protected void removeAttributeInternal(String name, boolean notify, 
boolean addDeltaRequest) {
         lockInternal();
diff --git a/java/org/apache/tomcat/websocket/WsSession.java 
b/java/org/apache/tomcat/websocket/WsSession.java
index 953250196b..4fc3ad22a4 100644
--- a/java/org/apache/tomcat/websocket/WsSession.java
+++ b/java/org/apache/tomcat/websocket/WsSession.java
@@ -278,6 +278,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the instance manager for this session.
+     *
      * @return the instance manager
      */
     public InstanceManager getInstanceManager() {
@@ -459,6 +460,7 @@ public class WsSession implements Session {
 
     /**
      * Checks if the session is closed.
+     *
      * @return true if the session is closed
      */
     public boolean isClosed() {
@@ -673,6 +675,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the session close timeout in milliseconds.
+     *
      * @return the session close timeout
      */
     protected long getSessionCloseTimeout() {
@@ -944,6 +947,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the user principal for this session.
+     *
      * @return the user principal
      */
     public Principal getUserPrincipalInternal() {
@@ -973,6 +977,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the local endpoint for this session.
+     *
      * @return the local endpoint
      */
     public Endpoint getLocal() {
@@ -981,9 +986,13 @@ public class WsSession implements Session {
 
 
     /**
-     * Returns the HTTP session ID associated with this WebSocket session.
+     * Returns the HTTP session ID associated with this WebSocket session at 
the time the WebSocket session was created.
+     *
      * @return the HTTP session ID, or null if not associated
+     *
+     * @deprecated Unused. Will be removed from Tomcat 12 onwards
      */
+    @Deprecated
     public String getHttpSessionId() {
         return httpSessionId;
     }
@@ -991,6 +1000,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the text message handler for this session.
+     *
      * @return the text message handler
      */
     protected MessageHandler getTextMessageHandler() {
@@ -1000,6 +1010,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the binary message handler for this session.
+     *
      * @return the binary message handler
      */
     protected MessageHandler getBinaryMessageHandler() {
@@ -1009,6 +1020,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the pong message handler for this session.
+     *
      * @return the pong message handler
      */
     protected MessageHandler.Whole<PongMessage> getPongMessageHandler() {
diff --git 
a/java/org/apache/tomcat/websocket/server/WsHttpSessionBindingListener.java 
b/java/org/apache/tomcat/websocket/server/WsHttpSessionBindingListener.java
new file mode 100644
index 0000000000..adefa660fa
--- /dev/null
+++ b/java/org/apache/tomcat/websocket/server/WsHttpSessionBindingListener.java
@@ -0,0 +1,47 @@
+/*
+ * 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.Serializable;
+
+import jakarta.servlet.http.HttpSession;
+import jakarta.servlet.http.HttpSessionBindingEvent;
+import jakarta.servlet.http.HttpSessionBindingListener;
+
+public record WsHttpSessionBindingListener(String key) implements 
HttpSessionBindingListener, Serializable {
+
+    @Override
+    public void valueUnbound(HttpSessionBindingEvent event) {
+        HttpSession httpSession = event.getSession();
+        /*
+         * During replication this event will be triggered when the attribute 
is updated. Updates should not trigger a
+         * call to the WebSocket server container. If this is an update, the 
session will still be valid.
+         */
+        try {
+            httpSession.getCreationTime();
+            // No exception. Session is valid. Nothing to do.
+            return;
+        } catch (IllegalStateException ise) {
+            // Ignore
+
+        }
+        Object obj = 
httpSession.getServletContext().getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
+        if (obj instanceof WsServerContainer wsServerContainer) {
+            wsServerContainer.handleHttpSessionKeyUnbound(key);
+        }
+    }
+}
diff --git a/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java 
b/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
index 3a31ace779..28205688a9 100644
--- a/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
+++ b/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
@@ -90,14 +90,14 @@ public class WsHttpUpgradeHandler implements 
InternalHttpUpgradeHandler {
     /**
      * Performs initialization before the WebSocket handshake is completed.
      *
-     * @param serverEndpointConfig the endpoint configuration
-     * @param wsc the WebSocket server container
-     * @param handshakeRequest the handshake request
+     * @param serverEndpointConfig       the endpoint configuration
+     * @param wsc                        the WebSocket server container
+     * @param handshakeRequest           the handshake request
      * @param negotiatedExtensionsPhase2 negotiated extensions
-     * @param subProtocol the negotiated sub-protocol
-     * @param transformation the data transformation
-     * @param pathParameters the path parameters
-     * @param secure whether the connection is secure
+     * @param subProtocol                the negotiated sub-protocol
+     * @param transformation             the data transformation
+     * @param pathParameters             the path parameters
+     * @param secure                     whether the connection is secure
      */
     public void preInit(ServerEndpointConfig serverEndpointConfig, 
WsServerContainer wsc,
             WsHandshakeRequest handshakeRequest, List<Extension> 
negotiatedExtensionsPhase2, String subProtocol,
@@ -158,7 +158,7 @@ public class WsHttpUpgradeHandler implements 
InternalHttpUpgradeHandler {
                 }
                 throw new IllegalArgumentException(t);
             }
-            webSocketContainer.registerSession(serverEndpointConfig.getPath(), 
wsSession);
+            webSocketContainer.registerSession(serverEndpointConfig.getPath(), 
wsSession, session);
         } catch (DeploymentException e) {
             throw new IllegalArgumentException(e);
         } finally {
diff --git a/java/org/apache/tomcat/websocket/server/WsSci.java 
b/java/org/apache/tomcat/websocket/server/WsSci.java
index 4520108296..918432b44c 100644
--- a/java/org/apache/tomcat/websocket/server/WsSci.java
+++ b/java/org/apache/tomcat/websocket/server/WsSci.java
@@ -137,7 +137,6 @@ public class WsSci implements ServletContainerInitializer {
 
         
servletContext.setAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE,
 sc);
 
-        servletContext.addListener(new WsSessionListener(sc));
         // Can't register the ContextListener again if the ContextListener is
         // calling this method
         if (initBySciMechanism) {
diff --git a/java/org/apache/tomcat/websocket/server/WsServerContainer.java 
b/java/org/apache/tomcat/websocket/server/WsServerContainer.java
index 43c907faac..aa332bec79 100644
--- a/java/org/apache/tomcat/websocket/server/WsServerContainer.java
+++ b/java/org/apache/tomcat/websocket/server/WsServerContainer.java
@@ -20,6 +20,8 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -33,6 +35,7 @@ import jakarta.servlet.ServletContext;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
 import jakarta.websocket.CloseReason;
 import jakarta.websocket.CloseReason.CloseCodes;
 import jakarta.websocket.DeploymentException;
@@ -69,7 +72,9 @@ public class WsServerContainer extends WsWebSocketContainer 
implements ServerCon
     private final Map<String,ExactPathMatch> configExactMatchMap = new 
ConcurrentHashMap<>();
     private final Map<Integer,ConcurrentSkipListMap<String,TemplatePathMatch>> 
configTemplateMatchMap =
             new ConcurrentHashMap<>();
-    private final Map<String,Set<WsSession>> authenticatedSessions = new 
ConcurrentHashMap<>();
+    private final Object authenticatedSessionMapLock = new Object();
+    private final Map<String,Set<WsSession>> httpSessionKeyToWebSocketSession 
= new HashMap<>();
+    private final Map<WsSession,String> webSocketSessionToHttpSessionKey = new 
HashMap<>();
     private volatile boolean endpointsRegistered = false;
     private volatile boolean deploymentFailed = false;
 
@@ -273,7 +278,9 @@ public class WsServerContainer extends WsWebSocketContainer 
implements ServerCon
 
     /**
      * Finds the endpoint configuration that matches the given path.
+     *
      * @param path the URI path to match
+     *
      * @return the mapping result, or null if no match is found
      */
     public WsMappingResult findMapping(String path) {
@@ -326,6 +333,7 @@ public class WsServerContainer extends WsWebSocketContainer 
implements ServerCon
 
     /**
      * Returns the write timeout handler.
+     *
      * @return the write timeout handler
      */
     protected WsWriteTimeout getTimeout() {
@@ -342,14 +350,10 @@ public class WsServerContainer extends 
WsWebSocketContainer implements ServerCon
     }
 
 
-    /**
-     * {@inheritDoc} Overridden to make it visible to other classes in this 
package.
-     */
-    @Override
-    protected void registerSession(Object key, WsSession wsSession) {
+    protected void registerSession(Object key, WsSession wsSession, Object 
httpSession) {
         super.registerSession(key, wsSession);
-        if (wsSession.isOpen() && wsSession.getUserPrincipal() != null && 
wsSession.getHttpSessionId() != null) {
-            registerAuthenticatedSession(wsSession, 
wsSession.getHttpSessionId());
+        if (wsSession.isOpen() && wsSession.getUserPrincipal() != null && 
httpSession != null) {
+            registerAuthenticatedSession(wsSession, (HttpSession) httpSession);
         }
     }
 
@@ -359,53 +363,106 @@ public class WsServerContainer extends 
WsWebSocketContainer implements ServerCon
      */
     @Override
     protected void unregisterSession(Object key, WsSession wsSession) {
-        if (wsSession.getUserPrincipalInternal() != null && 
wsSession.getHttpSessionId() != null) {
-            unregisterAuthenticatedSession(wsSession, 
wsSession.getHttpSessionId());
+        if (wsSession.getUserPrincipalInternal() != null) {
+            unregisterAuthenticatedSession(wsSession);
         }
         super.unregisterSession(key, wsSession);
     }
 
 
-    private void registerAuthenticatedSession(WsSession wsSession, String 
httpSessionId) {
-        Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
-        if (wsSessions == null) {
-            wsSessions = ConcurrentHashMap.newKeySet();
-            authenticatedSessions.putIfAbsent(httpSessionId, wsSessions);
-            wsSessions = authenticatedSessions.get(httpSessionId);
+    private void registerAuthenticatedSession(WsSession wsSession, HttpSession 
httpSession) {
+        boolean mustCloseWsSession = false;
+        String httpSessionKey = null;
+
+        synchronized (authenticatedSessionMapLock) {
+            try {
+                boolean mustAddSessionAttribute = false;
+                WsHttpSessionBindingListener listener = 
(WsHttpSessionBindingListener) httpSession
+                        
.getAttribute(WsHttpSessionBindingListener.class.getCanonicalName());
+                if (listener == null) {
+                    httpSessionKey = httpSession.getId();
+                    mustAddSessionAttribute = true;
+                } else {
+                    httpSessionKey = listener.key();
+                }
+                if (mustAddSessionAttribute) {
+                    /*
+                     * It is possible that the session expires concurrently 
with the attribute being added. Depending on
+                     * the exact timing, one of two things will happen. Either 
an IllegalStateException will be thrown
+                     * or the attribute will be added and then immediately 
removed from the session. Handle both of
+                     * these scenarios here.
+                     */
+                    
httpSession.setAttribute(WsHttpSessionBindingListener.class.getCanonicalName(),
+                            new WsHttpSessionBindingListener(httpSessionKey));
+                    if 
(httpSession.getAttribute(WsHttpSessionBindingListener.class.getCanonicalName())
 == null) {
+                        mustCloseWsSession = true;
+                    }
+                }
+            } catch (IllegalStateException ise) {
+                // Failing to set the attribute indicates that the session has 
already expired
+                mustCloseWsSession = true;
+            }
+
+            if (!mustCloseWsSession) {
+                Set<WsSession> wsSessions = 
httpSessionKeyToWebSocketSession.get(httpSessionKey);
+                if (wsSessions == null) {
+                    wsSessions = new HashSet<>();
+                    httpSessionKeyToWebSocketSession.put(httpSessionKey, 
wsSessions);
+                }
+                wsSessions.add(wsSession);
+                webSocketSessionToHttpSessionKey.put(wsSession, 
httpSessionKey);
+            }
+        }
+
+        if (mustCloseWsSession) {
+            closeAuthenticatedWebSocketSession(wsSession);
         }
-        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);
+    private void unregisterAuthenticatedSession(WsSession wsSession) {
+        synchronized (authenticatedSessionMapLock) {
+            String httpSessionKey = 
webSocketSessionToHttpSessionKey.remove(wsSession);
+            if (httpSessionKey != null) {
+                Set<WsSession> wsSessions = 
httpSessionKeyToWebSocketSession.get(httpSessionKey);
+                if (wsSessions != null) {
+                    wsSessions.remove(wsSession);
+                }
+            }
         }
     }
 
 
     /**
      * Closes all WebSocket sessions associated with the given authenticated 
HTTP session.
-     * @param httpSessionId the HTTP session ID
+     *
+     * @param httpSessionKey the HTTP session key
      */
-    public void closeAuthenticatedSession(String httpSessionId) {
-        Set<WsSession> wsSessions = 
authenticatedSessions.remove(httpSessionId);
+    public void handleHttpSessionKeyUnbound(String httpSessionKey) {
+        Set<WsSession> wsSessions;
+
+        synchronized (authenticatedSessionMapLock) {
+            wsSessions = 
httpSessionKeyToWebSocketSession.remove(httpSessionKey);
+        }
 
-        if (wsSessions != null && !wsSessions.isEmpty()) {
+        if (wsSessions != null) {
             for (WsSession wsSession : wsSessions) {
-                try {
-                    wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
-                } catch (IOException ignore) {
-                    // Any IOExceptions during close will have been caught and 
the
-                    // onError method called.
-                }
+                closeAuthenticatedWebSocketSession(wsSession);
             }
         }
     }
 
 
+    private void closeAuthenticatedWebSocketSession(WsSession wsSession) {
+        try {
+            wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
+        } catch (IOException ignore) {
+            // 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 {
 
diff --git a/java/org/apache/tomcat/websocket/server/WsSessionListener.java 
b/java/org/apache/tomcat/websocket/server/WsSessionListener.java
deleted file mode 100644
index a6edfe6e3c..0000000000
--- a/java/org/apache/tomcat/websocket/server/WsSessionListener.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 jakarta.servlet.http.HttpSessionEvent;
-import jakarta.servlet.http.HttpSessionListener;
-
-/**
- * Listener for HTTP session events to manage authenticated WebSocket sessions.
- * @param wsServerContainer the server container
- */
-public record WsSessionListener(WsServerContainer wsServerContainer) 
implements HttpSessionListener {
-    @Override
-    public void sessionDestroyed(HttpSessionEvent se) {
-        wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
-    }
-}
diff --git a/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java 
b/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
index d36e62e0e2..8aa374629d 100644
--- a/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
+++ b/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
@@ -16,17 +16,24 @@
  */
 package org.apache.tomcat.websocket;
 
+import java.io.IOException;
 import java.net.URI;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.websocket.ClientEndpointConfig;
 import jakarta.websocket.ClientEndpointConfig.Configurator;
 import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.HandshakeResponse;
 import jakarta.websocket.Session;
 import jakarta.websocket.WebSocketContainer;
 
@@ -37,10 +44,12 @@ import org.apache.catalina.Context;
 import org.apache.catalina.authenticator.AuthenticatorBase;
 import org.apache.catalina.servlets.DefaultServlet;
 import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.descriptor.web.LoginConfig;
 import org.apache.tomcat.util.descriptor.web.SecurityCollection;
 import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
 import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText;
+import org.apache.tomcat.websocket.TesterMessageCountClient.TesterEndpoint;
 import 
org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint;
 
 public class TestWebSocketFrameClient extends WebSocketBaseTest {
@@ -64,10 +73,10 @@ public class TestWebSocketFrameClient extends 
WebSocketBaseTest {
         WebSocketContainer wsContainer = 
ContainerProvider.getWebSocketContainer();
 
         // BZ 62596
-        ClientEndpointConfig clientEndpointConfig = 
ClientEndpointConfig.Builder.create()
-                .configurator(new Configurator() {
+        ClientEndpointConfig clientEndpointConfig =
+                ClientEndpointConfig.Builder.create().configurator(new 
Configurator() {
                     @Override
-                    public void beforeRequest(Map<String, List<String>> 
headers) {
+                    public void beforeRequest(Map<String,List<String>> 
headers) {
                         headers.put("Dummy",
                                 Collections.singletonList(String.join("", 
Collections.nCopies(4000, "A"))));
                         super.beforeRequest(headers);
@@ -178,6 +187,133 @@ public class TestWebSocketFrameClient extends 
WebSocketBaseTest {
         echoTester(URI_PROTECTED, clientEndpointConfig);
     }
 
+    @Test
+    public void 
testAuthenticatedWebSocketClosedWhenHttpSessionEndsWithoutRotatedSession() 
throws Exception {
+        doTestAuthenticatedWebSocketClosedWhenHttpSessionEnds(false);
+    }
+
+
+    @Test
+    public void 
testAuthenticatedWebSocketClosedWhenHttpSessionEndsWithRotatedSession() throws 
Exception {
+        doTestAuthenticatedWebSocketClosedWhenHttpSessionEnds(true);
+    }
+
+
+    private void doTestAuthenticatedWebSocketClosedWhenHttpSessionEnds(boolean 
rotateSessionID) throws Exception {
+
+        Tomcat tomcat = getTomcatInstance();
+        Context ctx = tomcat.addContext(URI_PROTECTED, null);
+        ctx.addApplicationListener(TesterEchoServer.Config.class.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMappingDecoded("/", "default");
+        Tomcat.addServlet(ctx, "invalidate", new HttpServlet() {
+
+            @Override
+            protected void doGet(HttpServletRequest req, HttpServletResponse 
resp) throws IOException {
+                req.getSession(false).invalidate();
+            }
+        });
+        ctx.addServletMappingDecoded("/invalidate", "invalidate");
+        Tomcat.addServlet(ctx, "changeSessionID", new HttpServlet() {
+
+            @Override
+            protected void doGet(HttpServletRequest req, HttpServletResponse 
resp) throws IOException {
+                req.changeSessionId();
+            }
+        });
+        ctx.addServletMappingDecoded("/changeSessionID", "changeSessionID");
+
+        SecurityCollection collection = new SecurityCollection();
+        collection.addPatternDecoded("/*");
+
+        tomcat.addUser(USER, PWD);
+        tomcat.addRole(USER, ROLE);
+
+        SecurityConstraint sc = new SecurityConstraint();
+        sc.addAuthRole(ROLE);
+        sc.addCollection(collection);
+        ctx.addConstraint(sc);
+
+        LoginConfig lc = new LoginConfig();
+        lc.setAuthMethod("BASIC");
+        ctx.setLoginConfig(lc);
+
+        AuthenticatorBase basicAuthenticator = new 
org.apache.catalina.authenticator.BasicAuthenticator();
+        basicAuthenticator.setAlwaysUseSession(true);
+        ctx.getPipeline().addValve(basicAuthenticator);
+
+        tomcat.start();
+
+        AtomicReference<String> sessionCookie = new AtomicReference<>();
+        ClientEndpointConfig clientEndpointConfig =
+                ClientEndpointConfig.Builder.create().configurator(new 
Configurator() {
+
+                    @Override
+                    public void afterResponse(HandshakeResponse hr) {
+                        List<String> cookies = 
hr.getHeaders().get("Set-Cookie");
+                        if (cookies != null) {
+                            for (String cookie : cookies) {
+                                if (cookie.startsWith("JSESSIONID=")) {
+                                    sessionCookie.set(cookie.split(";", 2)[0]);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }).build();
+        
clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME,
 USER);
+        
clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD,
 PWD);
+
+        WebSocketContainer wsContainer = 
ContainerProvider.getWebSocketContainer();
+        Session wsSession = 
wsContainer.connectToServer(TesterProgrammaticEndpoint.class, 
clientEndpointConfig,
+                new URI("ws://localhost:" + getPort() + URI_PROTECTED + 
TesterEchoServer.Config.PATH_BASIC));
+
+        CountDownLatch messageLatch = new CountDownLatch(1);
+        BasicText handler = new BasicText(messageLatch);
+        wsSession.addMessageHandler(handler);
+        wsSession.getBasicRemote().sendText("Hello");
+        Assert.assertTrue(messageLatch.await(10, TimeUnit.SECONDS));
+        Assert.assertEquals("Hello", handler.getMessages().poll());
+
+        if (rotateSessionID) {
+            Assert.assertNotNull(sessionCookie.get());
+            Map<String,List<String>> requestHeaders = new HashMap<>();
+            requestHeaders.put("Cookie", List.of(sessionCookie.get()));
+            Map<String,List<String>> responseHeaders = new HashMap<>();
+            int status = getUrl("http://localhost:"; + getPort() + 
URI_PROTECTED + "/changeSessionID", new ByteChunk(),
+                    requestHeaders, responseHeaders);
+            List<String> cookies = responseHeaders.get("Set-Cookie");
+            Assert.assertNotNull(cookies);
+            Assert.assertEquals(1, cookies.size());
+            sessionCookie.set(cookies.get(0).split(";", 2)[0]);
+            Assert.assertEquals(HttpServletResponse.SC_OK, status);
+
+            // CyclicBarrier would be cleaner but that requires a larger 
refactoring
+            wsSession.removeMessageHandler(handler);
+            CountDownLatch messageLatch2 = new CountDownLatch(1);
+            BasicText handler2 = new BasicText(messageLatch2);
+            wsSession.addMessageHandler(handler2);
+            wsSession.getBasicRemote().sendText("Hello");
+            Assert.assertTrue(messageLatch2.await(10, TimeUnit.SECONDS));
+            Assert.assertEquals("Hello", handler2.getMessages().poll());
+        }
+
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        TesterEndpoint endpoint = (TesterEndpoint) 
wsSession.getUserProperties().get("endpoint");
+        endpoint.setLatch(closeLatch);
+
+        Assert.assertNotNull(sessionCookie.get());
+        Map<String,List<String>> requestHeaders = new HashMap<>();
+        requestHeaders.put("Cookie", List.of(sessionCookie.get()));
+        int status = getUrl("http://localhost:"; + getPort() + URI_PROTECTED + 
"/invalidate", new ByteChunk(),
+                requestHeaders, null);
+        Assert.assertEquals(HttpServletResponse.SC_OK, status);
+
+        Assert.assertTrue(closeLatch.await(10, TimeUnit.SECONDS));
+        Assert.assertFalse(wsSession.isOpen());
+    }
+
+
     @Test
     public void testConnectToDigestEndpoint() throws Exception {
 
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d41b0b3cd5..dfa05843bb 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -494,6 +494,11 @@
         template ends in a variable without a trailing slash, that variable
         might be expanded to the empty string. (markt)
       </fix>
+      <fix>
+        Account for session ID changes when tracking WebSocket connections for
+        closure because they were created under an authenticated HTTP session
+        that has since ended. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Web applications">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to