Issue #3422 - WebSocket wss CLOSE_WAIT on aborted client connection

+ Adding testcase to replicate
+ Fixing CLOSE_WAIT by issuing wsclose + disconnect on eof

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2019-03-15 11:50:41 -05:00
parent 3b7338888b
commit 76de1c0f24
2 changed files with 45 additions and 3 deletions

View File

@ -20,8 +20,10 @@ package org.eclipse.jetty.websocket.tests.client;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
@ -36,6 +38,10 @@ import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketListener; import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.api.util.WSURI;
import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
@ -43,16 +49,20 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
* Tests for conditions due to bad networking. * Tests for conditions due to bad networking.
*/ */
public class BadNetworkTest public class BadNetworkTest
{ {
private static final Logger LOG = Log.getLogger(BadNetworkTest.class);
private Server server; private Server server;
private WebSocketClient client; private WebSocketClient client;
private ServletContextHandler context;
@BeforeEach @BeforeEach
public void startClient() throws Exception public void startClient() throws Exception
@ -71,16 +81,17 @@ public class BadNetworkTest
connector.setPort(0); connector.setPort(0);
server.addConnector(connector); server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(); context = new ServletContextHandler();
context.setContextPath("/"); context.setContextPath("/");
ServletHolder holder = new ServletHolder(new WebSocketServlet() ServletHolder holder = new ServletHolder(new WebSocketServlet()
{ {
@Override @Override
public void configure(WebSocketServletFactory factory) public void configure(WebSocketServletFactory factory)
{ {
factory.getPolicy().setIdleTimeout(10000); factory.getPolicy().setIdleTimeout(100000);
factory.getPolicy().setMaxTextMessageSize(1024 * 1024 * 2); factory.getPolicy().setMaxTextMessageSize(1024 * 1024 * 2);
factory.register(ServerEndpoint.class); factory.setCreator((req, resp) -> new ServerEndpoint());
} }
}); });
context.addServlet(holder, "/ws"); context.addServlet(holder, "/ws");
@ -108,6 +119,23 @@ public class BadNetworkTest
@Test @Test
public void testAbruptClientClose() throws Exception public void testAbruptClientClose() throws Exception
{ {
AtomicReference<WebSocketSession> serverSessionRef = new AtomicReference<>();
CountDownLatch serverCloseLatch = new CountDownLatch(1);
WebSocketServerFactory wssf = (WebSocketServerFactory) context.getServletContext().getAttribute(WebSocketServletFactory.class.getName());
wssf.addSessionListener(new WebSocketSessionListener() {
@Override
public void onSessionOpened(WebSocketSession session)
{
serverSessionRef.set(session);
}
@Override
public void onSessionClosed(WebSocketSession session)
{
serverCloseLatch.countDown();
}
});
CloseTrackingEndpoint wsocket = new CloseTrackingEndpoint(); CloseTrackingEndpoint wsocket = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws")); URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
@ -116,14 +144,24 @@ public class BadNetworkTest
// Validate that we are connected // Validate that we are connected
future.get(30,TimeUnit.SECONDS); future.get(30,TimeUnit.SECONDS);
WebSocketSession serverSession = serverSessionRef.get();
// Have client disconnect abruptly // Have client disconnect abruptly
Session session = wsocket.getSession(); Session session = wsocket.getSession();
LOG.info("client.disconnect");
session.disconnect(); session.disconnect();
// Client Socket should see a close event, with status NO_CLOSE // Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection // This event is automatically supplied by the underlying WebSocketClientConnection
// in the situation of a bad network connection. // in the situation of a bad network connection.
wsocket.assertReceivedCloseEvent(5000, is(StatusCode.NO_CLOSE), containsString("")); wsocket.assertReceivedCloseEvent(5000, is(StatusCode.NO_CLOSE), containsString(""));
TimeUnit.SECONDS.sleep(1); // Let server side close connection
assertTrue(serverCloseLatch.await(1, TimeUnit.SECONDS), "Server Session Close should have happened");
AbstractWebSocketConnection conn = (AbstractWebSocketConnection) serverSession.getConnection();
assertThat("Connection.isOpen", conn.isOpen(), is(false));
} }
@Test @Test

View File

@ -446,6 +446,10 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
if (readMode == ReadMode.EOF) if (readMode == ReadMode.EOF)
{ {
readState.eof(); readState.eof();
// Handle case where the remote connection was abruptly terminated without a close frame
CloseInfo close = new CloseInfo(StatusCode.SHUTDOWN);
close(close, new DisconnectCallback(this));
} }
else if (!readState.suspend()) else if (!readState.suspend())
{ {