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">
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.